mirror of
https://github.com/DioxusLabs/dioxus
synced 2024-11-26 14:10:20 +00:00
Suspense boundaries/out of order streaming/anyhow like error handling (#2365)
* create static site generation helpers in the router crate * work on integrating static site generation into fullstack * move ssg into a separate crate * integrate ssg with the launch builder * simplify ssg example * fix static_routes for child routes * move CLI hot reloading websocket code into dioxus-hot-reload * fix some unused imports * use the same hot reloading websocket code for fullstack * fix fullstack hot reloading * move cli hot reloading logic into the hot reload crate * ssg example working with dx serve * add more examples * fix clippy * switch to a result for Element * fix formatting * fix hot reload doctest imports * fix axum imports * add show method to error context * implement retaining nodes during suspense * fix unterminated if statements * communicate between tasks and suspense boundaries * make suspense placeholders easier to use * implement IntoDynNode and IntoVNode for more wrappers * fix clippy examples * fix rsx tests * add streaming html utilities to the ssr package * unify hydration and non-hydration ssr cache * fix router with Result Element * don't run server doc tests * Fix hot reload websocket doc examples * simple apps working with fullstack streaming * fix preloading wasm * Report errors encountered while streaming * remove async from incremental renderer * document new VirtualDom suspense methods * make streaming work with incremental rendering * fix static site generation * document suspense structs * create closure type; allow async event handlers in props; allow shorthand event handlers * test forwarding event handlers with the shorthand syntax * fix clippy * fix imports in spawn async doctest * fix empty rsx * fix async result event handlers * fix mounting router in multiple places * Fix task dead cancel race condition * simplify diffing before adding suspense * fix binary size increase * fix attribute diffing * more diffing fixes * create minimal fullstack feature * smaller fullstack bundles * allow mounting nodes that are already created and creating nodes without mounting them * fix hot reload feature * fix replacing components * don't reclaim virtual nodes * client side suspense working! * fix CLI * slightly smaller fullstack builds * fix multiple suspended scopes * fix merge errors * yield back to tokio every few polls to fix suspending on many tasks at once * remove logs * document suspense boundary and update suspense example * fix ssg * make streaming optional * fix some router and core tests * fix suspense example * fix serialization with out of order server futures * add incremental streaming hackernews demo * fix hackernews demo * fix root hackernews redirect * fix formatting * add tests for suspense cases * slightly smaller binaries * slightly smaller * improve error handling docs * fix errors example link * fix doc tests * remove log file * fix ssr cache type inference * remove index.html * fix ssg render template * fix assigning ids on elements with dynamic attributes * add desktop feature to the workspace examples * remove router static generation example; ssg lives in the dioxus-static-generation package * add a test for effects during suspense * only run effects on mounted nodes * fix multiple suspense roots * fix node iterator * fix closures without arguments * fix dioxus-core readme doctest * remove suspense logs * fix scope stack * fix clippy * remove unused suspense boundary from hackernews * assert that launch never returns for better compiler errors * fix static generation launch function * fix web renderer * pass context providers into server functions * add an example for FromContext * clean up DioxusRouterExt * fix server function context * fix fullstack desktop example * forward CLI serve settings to fullstack * re-export serve config at the root of fullstack * forward env directly instead of using a guard * just set the port in the CLI for fullstack playwright tests * fix fullstack dioxus-cli-config feature * fix launch server merge conflicts * fix fullstack launch context * Merge branch 'main' into suspense-2.0 * fix fullstack html data * remove drop virtual dom feature * add a comment about only_write_templates binary size workaround * remove explicit dependencies from use_server_future * make ErrorContext and SuspenseContext more similar * Tweak: small tweaks to tomls to make diff smaller * only rerun components under suspense after the initial placeholders are sent to the client * add module docs for suspense * keep track of when suspense boundaries are resolved * start implementing JS out of order streaming * fix core tests * implement the server side of suspense with js * fix streaming ssr with nested suspense * move streaming ssr code into fullstack * revert minification changes * serialize server future data as the html streams * start loading scripts wasm immediately instead of defering the script * very basic nested suspense example working with minimal html updates * clean up some suspense/error docs * fix hydrating nested pending server futures * sort resolved boundaries by height * Fix disconnecting clients while streaming * fix static generation crate * don't insert extra divs when hydrating streamed chunks * wait to swap in the elements until they are hydrated * remove inline streaming script * hackernews partially working * fix spa mode * banish the open shadow dom * fix removing placeholder * set up streaming playwright test * run web playwright tests on 9999 to avoid port conflicts with other local servers * remove suspense nodes if the suspense boundary is replaced before the suspense resolves on the server * ignore hydration of removed suspense boundaries * use path based indexing to fix hydrating suspense after parent suspense with child is removed * re-export dioxus error * remove resolved suspense divs if the suspense boundary has been removed * Fix client side initialized server futures * ignore comment nodes while traversing nodes in core to avoid lists getting swapped out with suspense * Pass initial hydration data to the client * hide pre nodes * don't panic if reclaiming an element fails * fix scope stack when polling tasks * improve deserialization out of length message * Ok(VNode::placeholder()) -> VNode::empty() * fix typo in rsx usage * restore testing changes from suspense example * clean up some logs and comments * fix playwright tests * clean up more changes in core * clean up core tests * remove anymap dependency * clean up changes to hooks * clean up changes in the router, rsx, and web * revert changes to axum-hello-world * fix use_server_future * fix clippy in dioxus-core * check that the next or previous node exist before checking if we should ignore them * fix formatting * fix suspense playwright test * remove unused suspense code * add more suspense playwright tests * add more docs for error boundaries * fix suspense core tests * fix ErrorBoundary example * remove a bunch of debug logging in js * fix router failure_external_navigation * use absolute paths in the interpreter build.rs * strip '\r' while hashing ts files * add a wrapper with a default error boundary and suspense boundary * restore hot reloading * ignore non-ts files when hashing * sort ts files before hashing them * fix rsx tests * fix fullstack doc tests * fix core tests * fix axum auth example * update suspense hydration diagram * longer playwright build limit * tiny fixes - spelling, formatting * update diagram link * remove comment and template nodes for suspense placeholders * remove comment nodes as we hydrate text * simplify hackernews example * clean up hydrating text nodes * switch to a separate environment variable for the base path for smaller binaries * clean up file system html trait * fix form data * move streaming code into fullstack * implement serialize and deserialize for CapturedError * remove waits in the nested suspense playwright spec * force sequential fullstack builds for CI * longer nested suspense delay for CI * fix --force-sequential flag * wait to launch server until client build is done --------- Co-authored-by: Jonathan Kelley <jkelleyrtp@gmail.com>
This commit is contained in:
parent
ffa36a67c3
commit
022e4ad203
175 changed files with 7874 additions and 4012 deletions
425
Cargo.lock
generated
425
Cargo.lock
generated
|
@ -64,7 +64,7 @@ version = "0.7.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
@ -77,7 +77,7 @@ checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"const-random",
|
"const-random",
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"version_check",
|
"version_check",
|
||||||
"zerocopy",
|
"zerocopy",
|
||||||
|
@ -320,6 +320,17 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-channel"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
|
||||||
|
dependencies = [
|
||||||
|
"concurrent-queue",
|
||||||
|
"event-listener 2.5.3",
|
||||||
|
"futures-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-channel"
|
name = "async-channel"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
@ -357,8 +368,8 @@ checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-task",
|
"async-task",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"fastrand",
|
"fastrand 2.0.2",
|
||||||
"futures-lite",
|
"futures-lite 2.3.0",
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -368,9 +379,44 @@ version = "2.1.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1"
|
checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock",
|
"async-lock 3.3.0",
|
||||||
"blocking",
|
"blocking",
|
||||||
"futures-lite",
|
"futures-lite 2.3.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-global-executor"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel 2.2.1",
|
||||||
|
"async-executor",
|
||||||
|
"async-io 2.3.2",
|
||||||
|
"async-lock 3.3.0",
|
||||||
|
"blocking",
|
||||||
|
"futures-lite 2.3.0",
|
||||||
|
"once_cell",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-io"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
|
||||||
|
dependencies = [
|
||||||
|
"async-lock 2.8.0",
|
||||||
|
"autocfg",
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"futures-lite 1.13.0",
|
||||||
|
"log",
|
||||||
|
"parking",
|
||||||
|
"polling 2.8.0",
|
||||||
|
"rustix 0.37.27",
|
||||||
|
"slab",
|
||||||
|
"socket2 0.4.10",
|
||||||
|
"waker-fn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -379,19 +425,28 @@ version = "2.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884"
|
checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-lock",
|
"async-lock 3.3.0",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-lite",
|
"futures-lite 2.3.0",
|
||||||
"parking",
|
"parking",
|
||||||
"polling",
|
"polling 3.7.0",
|
||||||
"rustix",
|
"rustix 0.38.34",
|
||||||
"slab",
|
"slab",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-lock"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
|
||||||
|
dependencies = [
|
||||||
|
"event-listener 2.5.3",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-lock"
|
name = "async-lock"
|
||||||
version = "3.3.0"
|
version = "3.3.0"
|
||||||
|
@ -409,9 +464,9 @@ version = "2.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
|
checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-io",
|
"async-io 2.3.2",
|
||||||
"blocking",
|
"blocking",
|
||||||
"futures-lite",
|
"futures-lite 2.3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -420,16 +475,16 @@ version = "2.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d"
|
checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel 2.2.1",
|
||||||
"async-io",
|
"async-io 2.3.2",
|
||||||
"async-lock",
|
"async-lock 3.3.0",
|
||||||
"async-signal",
|
"async-signal",
|
||||||
"async-task",
|
"async-task",
|
||||||
"blocking",
|
"blocking",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"event-listener 5.3.0",
|
"event-listener 5.3.0",
|
||||||
"futures-lite",
|
"futures-lite 2.3.0",
|
||||||
"rustix",
|
"rustix 0.38.34",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
@ -451,18 +506,44 @@ version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda"
|
checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-io",
|
"async-io 2.3.2",
|
||||||
"async-lock",
|
"async-lock 3.3.0",
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"rustix",
|
"rustix 0.38.34",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"slab",
|
"slab",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-std"
|
||||||
|
version = "1.12.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d"
|
||||||
|
dependencies = [
|
||||||
|
"async-channel 1.9.0",
|
||||||
|
"async-global-executor",
|
||||||
|
"async-io 1.13.0",
|
||||||
|
"async-lock 2.8.0",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"futures-lite 1.13.0",
|
||||||
|
"gloo-timers",
|
||||||
|
"kv-log-macro",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"once_cell",
|
||||||
|
"pin-project-lite",
|
||||||
|
"pin-utils",
|
||||||
|
"slab",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-task"
|
name = "async-task"
|
||||||
version = "4.7.0"
|
version = "4.7.0"
|
||||||
|
@ -689,7 +770,7 @@ name = "axum-hello-world"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"serde",
|
"serde",
|
||||||
"simple_logger",
|
"simple_logger",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -942,12 +1023,12 @@ version = "1.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-channel",
|
"async-channel 2.2.1",
|
||||||
"async-lock",
|
"async-lock 3.3.0",
|
||||||
"async-task",
|
"async-task",
|
||||||
"fastrand",
|
"fastrand 2.0.2",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"futures-lite",
|
"futures-lite 2.3.0",
|
||||||
"piper",
|
"piper",
|
||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
@ -1535,7 +1616,7 @@ version = "0.1.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"tiny-keccak",
|
"tiny-keccak",
|
||||||
]
|
]
|
||||||
|
@ -2223,7 +2304,7 @@ dependencies = [
|
||||||
"openssl",
|
"openssl",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"rayon",
|
"rayon",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"rsx-rosetta",
|
"rsx-rosetta",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -2291,6 +2372,7 @@ name = "dioxus-core"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dioxus",
|
"dioxus",
|
||||||
|
"dioxus-html",
|
||||||
"dioxus-ssr",
|
"dioxus-ssr",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
@ -2298,7 +2380,7 @@ dependencies = [
|
||||||
"longest-increasing-subsequence",
|
"longest-increasing-subsequence",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"serde",
|
"serde",
|
||||||
|
@ -2327,12 +2409,6 @@ dependencies = [
|
||||||
"trybuild",
|
"trybuild",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dioxus-debug-cell"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2ea539174bb236e0e7dc9c12b19b88eae3cb574dedbd0252a2d43ea7e6de13e2"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-desktop"
|
name = "dioxus-desktop"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -2360,7 +2436,7 @@ dependencies = [
|
||||||
"objc",
|
"objc",
|
||||||
"objc_id",
|
"objc_id",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"separator",
|
"separator",
|
||||||
|
@ -2387,11 +2463,11 @@ dependencies = [
|
||||||
"dioxus-ssr",
|
"dioxus-ssr",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
"http-range",
|
"http-range",
|
||||||
"manganis",
|
"manganis",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"separator",
|
"separator",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -2422,18 +2498,20 @@ dependencies = [
|
||||||
"dioxus-cli-config",
|
"dioxus-cli-config",
|
||||||
"dioxus-desktop",
|
"dioxus-desktop",
|
||||||
"dioxus-hot-reload",
|
"dioxus-hot-reload",
|
||||||
|
"dioxus-interpreter-js",
|
||||||
"dioxus-lib",
|
"dioxus-lib",
|
||||||
"dioxus-mobile",
|
"dioxus-mobile",
|
||||||
"dioxus-ssr",
|
"dioxus-ssr",
|
||||||
"dioxus-web",
|
"dioxus-web",
|
||||||
"dioxus_server_macro",
|
"dioxus_server_macro",
|
||||||
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"http 1.1.0",
|
"http 1.1.0",
|
||||||
"hyper 1.3.1",
|
"hyper 1.3.1",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
"parking_lot",
|
||||||
"pin-project",
|
"pin-project",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
|
||||||
"server_fn",
|
"server_fn",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
@ -2447,21 +2525,32 @@ dependencies = [
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dioxus-hackernews"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"dioxus",
|
||||||
|
"reqwest 0.12.4",
|
||||||
|
"serde",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
"tracing-wasm",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dioxus-hooks"
|
name = "dioxus-hooks"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-core 0.5.2",
|
"dioxus-core 0.5.2",
|
||||||
"dioxus-debug-cell",
|
|
||||||
"dioxus-signals",
|
"dioxus-signals",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"generational-box 0.5.2",
|
"generational-box 0.5.2",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
"slab",
|
"slab",
|
||||||
"thiserror",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
|
@ -2503,6 +2592,7 @@ dependencies = [
|
||||||
"euclid",
|
"euclid",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"generational-box 0.5.2",
|
"generational-box 0.5.2",
|
||||||
|
"js-sys",
|
||||||
"keyboard-types",
|
"keyboard-types",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rustversion",
|
"rustversion",
|
||||||
|
@ -2700,7 +2790,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"simple_logger",
|
"simple_logger",
|
||||||
|
@ -2722,6 +2812,7 @@ dependencies = [
|
||||||
"dioxus-cli-config",
|
"dioxus-cli-config",
|
||||||
"dioxus-core 0.5.2",
|
"dioxus-core 0.5.2",
|
||||||
"dioxus-html",
|
"dioxus-html",
|
||||||
|
"dioxus-interpreter-js",
|
||||||
"dioxus-signals",
|
"dioxus-signals",
|
||||||
"fern",
|
"fern",
|
||||||
"fs_extra",
|
"fs_extra",
|
||||||
|
@ -2768,7 +2859,7 @@ dependencies = [
|
||||||
name = "dioxus-web"
|
name = "dioxus-web"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"ciborium",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"dioxus",
|
"dioxus",
|
||||||
"dioxus-core 0.5.2",
|
"dioxus-core 0.5.2",
|
||||||
|
@ -3191,6 +3282,15 @@ version = "0.9.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
|
checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastrand"
|
||||||
|
version = "1.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
|
||||||
|
dependencies = [
|
||||||
|
"instant",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.0.2"
|
version = "2.0.2"
|
||||||
|
@ -3451,13 +3551,28 @@ version = "0.3.30"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-lite"
|
||||||
|
version = "1.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
|
||||||
|
dependencies = [
|
||||||
|
"fastrand 1.9.0",
|
||||||
|
"futures-core",
|
||||||
|
"futures-io",
|
||||||
|
"memchr",
|
||||||
|
"parking",
|
||||||
|
"pin-project-lite",
|
||||||
|
"waker-fn",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-lite"
|
name = "futures-lite"
|
||||||
version = "2.3.0"
|
version = "2.3.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand 2.0.2",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
"parking",
|
"parking",
|
||||||
|
@ -3663,9 +3778,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.14"
|
version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
|
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
|
@ -3976,7 +4091,7 @@ version = "0.1.12"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc"
|
checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastrand",
|
"fastrand 2.0.2",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -4226,6 +4341,8 @@ version = "0.2.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
|
checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"futures-channel",
|
||||||
|
"futures-core",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
@ -4740,7 +4857,7 @@ dependencies = [
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa 1.0.11",
|
"itoa 1.0.11",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2 0.5.6",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -4814,6 +4931,22 @@ dependencies = [
|
||||||
"tokio-native-tls",
|
"tokio-native-tls",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-tls"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper 1.3.1",
|
||||||
|
"hyper-util",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
@ -4827,7 +4960,7 @@ dependencies = [
|
||||||
"http-body 1.0.0",
|
"http-body 1.0.0",
|
||||||
"hyper 1.3.1",
|
"hyper 1.3.1",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2 0.5.6",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
@ -5137,16 +5270,27 @@ version = "0.3.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
|
checksum = "f958d3d68f4167080a18141e10381e7634563984a537f2a49a30fd8e53ac5767"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "io-lifetimes"
|
||||||
|
version = "1.0.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
|
||||||
|
dependencies = [
|
||||||
|
"hermit-abi 0.3.9",
|
||||||
|
"libc",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipconfig"
|
name = "ipconfig"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
|
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"socket2",
|
"socket2 0.5.6",
|
||||||
"widestring",
|
"widestring",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
"winreg",
|
"winreg 0.50.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -5391,6 +5535,15 @@ dependencies = [
|
||||||
"selectors",
|
"selectors",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "kv-log-macro"
|
||||||
|
version = "1.0.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
|
||||||
|
dependencies = [
|
||||||
|
"log",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
|
@ -5414,9 +5567,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.153"
|
version = "0.2.155"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libflate"
|
name = "libflate"
|
||||||
|
@ -5548,7 +5701,7 @@ dependencies = [
|
||||||
"cssparser-color",
|
"cssparser-color",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"data-encoding",
|
"data-encoding",
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
"itertools 0.10.5",
|
"itertools 0.10.5",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"parcel_selectors",
|
"parcel_selectors",
|
||||||
|
@ -5587,6 +5740,12 @@ version = "0.5.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "linux-raw-sys"
|
||||||
|
version = "0.3.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.13"
|
version = "0.4.13"
|
||||||
|
@ -5776,7 +5935,7 @@ dependencies = [
|
||||||
"railwind",
|
"railwind",
|
||||||
"ravif",
|
"ravif",
|
||||||
"rayon",
|
"rayon",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"toml 0.7.8",
|
"toml 0.7.8",
|
||||||
|
@ -5794,7 +5953,7 @@ dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"home",
|
"home",
|
||||||
"infer 0.11.0",
|
"infer 0.11.0",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"serde",
|
"serde",
|
||||||
"toml 0.7.8",
|
"toml 0.7.8",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
@ -6109,6 +6268,15 @@ dependencies = [
|
||||||
"jni-sys",
|
"jni-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "nested-suspense"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"dioxus",
|
||||||
|
"serde",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "new_debug_unreachable"
|
name = "new_debug_unreachable"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
|
@ -6905,7 +7073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
|
checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"atomic-waker",
|
"atomic-waker",
|
||||||
"fastrand",
|
"fastrand 2.0.2",
|
||||||
"futures-io",
|
"futures-io",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -6991,6 +7159,22 @@ dependencies = [
|
||||||
"miniz_oxide",
|
"miniz_oxide",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "polling"
|
||||||
|
version = "2.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"cfg-if",
|
||||||
|
"concurrent-queue",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"pin-project-lite",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "polling"
|
name = "polling"
|
||||||
version = "3.7.0"
|
version = "3.7.0"
|
||||||
|
@ -7001,7 +7185,7 @@ dependencies = [
|
||||||
"concurrent-queue",
|
"concurrent-queue",
|
||||||
"hermit-abi 0.3.9",
|
"hermit-abi 0.3.9",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"rustix",
|
"rustix 0.38.34",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
@ -7311,7 +7495,7 @@ version = "0.6.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -7438,7 +7622,7 @@ version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
|
checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
"libredox",
|
"libredox",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
@ -7529,7 +7713,7 @@ dependencies = [
|
||||||
"http-body 0.4.6",
|
"http-body 0.4.6",
|
||||||
"hyper 0.14.28",
|
"hyper 0.14.28",
|
||||||
"hyper-rustls 0.24.2",
|
"hyper-rustls 0.24.2",
|
||||||
"hyper-tls",
|
"hyper-tls 0.5.0",
|
||||||
"ipnet",
|
"ipnet",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
|
@ -7557,7 +7741,49 @@ dependencies = [
|
||||||
"wasm-streams",
|
"wasm-streams",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
"webpki-roots 0.25.4",
|
"webpki-roots 0.25.4",
|
||||||
"winreg",
|
"winreg 0.50.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.0",
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"h2 0.4.4",
|
||||||
|
"http 1.1.0",
|
||||||
|
"http-body 1.0.0",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper 1.3.1",
|
||||||
|
"hyper-tls 0.6.0",
|
||||||
|
"hyper-util",
|
||||||
|
"ipnet",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"native-tls",
|
||||||
|
"once_cell",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"rustls-pemfile 2.1.2",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper 0.1.2",
|
||||||
|
"system-configuration",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
"winreg 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -7638,7 +7864,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
"libc",
|
"libc",
|
||||||
"spin 0.9.8",
|
"spin 0.9.8",
|
||||||
"untrusted",
|
"untrusted",
|
||||||
|
@ -7773,6 +7999,20 @@ dependencies = [
|
||||||
"semver",
|
"semver",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustix"
|
||||||
|
version = "0.37.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"errno",
|
||||||
|
"io-lifetimes",
|
||||||
|
"libc",
|
||||||
|
"linux-raw-sys 0.3.8",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustix"
|
name = "rustix"
|
||||||
version = "0.38.34"
|
version = "0.38.34"
|
||||||
|
@ -7782,7 +8022,7 @@ dependencies = [
|
||||||
"bitflags 2.5.0",
|
"bitflags 2.5.0",
|
||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys 0.4.13",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -8185,7 +8425,7 @@ dependencies = [
|
||||||
"inventory",
|
"inventory",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"reqwest",
|
"reqwest 0.11.27",
|
||||||
"send_wrapper",
|
"send_wrapper",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
@ -8437,6 +8677,16 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "socket2"
|
||||||
|
version = "0.4.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
version = "0.5.6"
|
version = "0.5.6"
|
||||||
|
@ -8846,6 +9096,15 @@ dependencies = [
|
||||||
"is_ci",
|
"is_ci",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "suspense-carousel"
|
||||||
|
version = "0.5.2"
|
||||||
|
dependencies = [
|
||||||
|
"async-std",
|
||||||
|
"dioxus",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "1.0.109"
|
version = "1.0.109"
|
||||||
|
@ -9036,7 +9295,7 @@ dependencies = [
|
||||||
"uuid",
|
"uuid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
"winreg",
|
"winreg 0.50.0",
|
||||||
"zip",
|
"zip",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -9084,8 +9343,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"fastrand",
|
"fastrand 2.0.2",
|
||||||
"rustix",
|
"rustix 0.38.34",
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -9268,7 +9527,7 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
"signal-hook-registry",
|
||||||
"socket2",
|
"socket2 0.5.6",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
@ -9792,7 +10051,7 @@ version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom 0.2.14",
|
"getrandom 0.2.15",
|
||||||
"serde",
|
"serde",
|
||||||
"sha1_smol",
|
"sha1_smol",
|
||||||
]
|
]
|
||||||
|
@ -9844,6 +10103,12 @@ version = "0.5.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
|
checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "waker-fn"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "walkdir"
|
name = "walkdir"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
|
@ -10291,7 +10556,7 @@ dependencies = [
|
||||||
"either",
|
"either",
|
||||||
"home",
|
"home",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix",
|
"rustix 0.38.34",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -10679,6 +10944,16 @@ dependencies = [
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winreg"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"windows-sys 0.48.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wry"
|
name = "wry"
|
||||||
version = "0.37.0"
|
version = "0.37.0"
|
||||||
|
@ -10761,8 +11036,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
|
checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys",
|
"linux-raw-sys 0.4.13",
|
||||||
"rustix",
|
"rustix 0.38.34",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -10796,8 +11071,8 @@ dependencies = [
|
||||||
"async-broadcast",
|
"async-broadcast",
|
||||||
"async-executor",
|
"async-executor",
|
||||||
"async-fs",
|
"async-fs",
|
||||||
"async-io",
|
"async-io 2.3.2",
|
||||||
"async-lock",
|
"async-lock 3.3.0",
|
||||||
"async-process",
|
"async-process",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"async-task",
|
"async-task",
|
||||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -35,6 +35,7 @@ members = [
|
||||||
"packages/fullstack/examples/axum-streaming",
|
"packages/fullstack/examples/axum-streaming",
|
||||||
"packages/fullstack/examples/axum-desktop",
|
"packages/fullstack/examples/axum-desktop",
|
||||||
"packages/fullstack/examples/axum-auth",
|
"packages/fullstack/examples/axum-auth",
|
||||||
|
"packages/fullstack/examples/hackernews",
|
||||||
"packages/static-generation/examples/simple",
|
"packages/static-generation/examples/simple",
|
||||||
"packages/static-generation/examples/router",
|
"packages/static-generation/examples/router",
|
||||||
"packages/static-generation/examples/github-pages",
|
"packages/static-generation/examples/github-pages",
|
||||||
|
@ -46,6 +47,8 @@ members = [
|
||||||
"packages/playwright-tests/liveview",
|
"packages/playwright-tests/liveview",
|
||||||
"packages/playwright-tests/web",
|
"packages/playwright-tests/web",
|
||||||
"packages/playwright-tests/fullstack",
|
"packages/playwright-tests/fullstack",
|
||||||
|
"packages/playwright-tests/suspense-carousel",
|
||||||
|
"packages/playwright-tests/nested-suspense",
|
||||||
]
|
]
|
||||||
exclude = ["examples/mobile_demo", "examples/openid_connect_demo"]
|
exclude = ["examples/mobile_demo", "examples/openid_connect_demo"]
|
||||||
|
|
||||||
|
@ -61,10 +64,10 @@ dioxus-core-macro = { path = "packages/core-macro", version = "0.5.0" }
|
||||||
dioxus-config-macro = { path = "packages/config-macro", version = "0.5.0" }
|
dioxus-config-macro = { path = "packages/config-macro", version = "0.5.0" }
|
||||||
dioxus-router = { path = "packages/router", version = "0.5.0" }
|
dioxus-router = { path = "packages/router", version = "0.5.0" }
|
||||||
dioxus-router-macro = { path = "packages/router-macro", version = "0.5.0" }
|
dioxus-router-macro = { path = "packages/router-macro", version = "0.5.0" }
|
||||||
dioxus-html = { path = "packages/html", version = "0.5.0" }
|
dioxus-html = { path = "packages/html", default-features = false, version = "0.5.0" }
|
||||||
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.5.0" }
|
dioxus-html-internal-macro = { path = "packages/html-internal-macro", version = "0.5.0" }
|
||||||
dioxus-hooks = { path = "packages/hooks", version = "0.5.0" }
|
dioxus-hooks = { path = "packages/hooks", version = "0.5.0" }
|
||||||
dioxus-web = { path = "packages/web", version = "0.5.0" }
|
dioxus-web = { path = "packages/web", default-features = false, version = "0.5.0" }
|
||||||
dioxus-ssr = { path = "packages/ssr", version = "0.5.0", default-features = false }
|
dioxus-ssr = { path = "packages/ssr", version = "0.5.0", default-features = false }
|
||||||
dioxus-desktop = { path = "packages/desktop", version = "0.5.0", default-features = false }
|
dioxus-desktop = { path = "packages/desktop", version = "0.5.0", default-features = false }
|
||||||
dioxus-mobile = { path = "packages/mobile", version = "0.5.0" }
|
dioxus-mobile = { path = "packages/mobile", version = "0.5.0" }
|
||||||
|
@ -118,6 +121,9 @@ axum_session_auth = "0.12.1"
|
||||||
axum-extra = "0.9.2"
|
axum-extra = "0.9.2"
|
||||||
reqwest = "0.11.24"
|
reqwest = "0.11.24"
|
||||||
owo-colors = "4.0.0"
|
owo-colors = "4.0.0"
|
||||||
|
ciborium = "0.2.1"
|
||||||
|
base64 = "0.21.0"
|
||||||
|
once_cell = "1.17.1"
|
||||||
|
|
||||||
# speed up some macros by optimizing them
|
# speed up some macros by optimizing them
|
||||||
[profile.dev.package.insta]
|
[profile.dev.package.insta]
|
||||||
|
@ -275,7 +281,7 @@ required-features = ["desktop"]
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "error_handle"
|
name = "errors"
|
||||||
required-features = ["desktop"]
|
required-features = ["desktop"]
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ cargo run --example hello_world
|
||||||
|
|
||||||
[disabled](./disabled.rs) - Disable buttons conditionally
|
[disabled](./disabled.rs) - Disable buttons conditionally
|
||||||
|
|
||||||
[error_handle](./error_handle.rs) - Handle errors with early return
|
[errors](./errors.rs) - Handle errors with early return
|
||||||
|
|
||||||
## Routing
|
## Routing
|
||||||
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
//! This example showcases how to use the ErrorBoundary component to handle errors in your app.
|
|
||||||
//!
|
|
||||||
//! The ErrorBoundary component is a special component that can be used to catch panics and other errors that occur.
|
|
||||||
//! By default, Dioxus will catch panics during rendering, async, and handlers, and bubble them up to the nearest
|
|
||||||
//! error boundary. If no error boundary is present, it will be caught by the root error boundary and the app will
|
|
||||||
//! render the error message as just a string.
|
|
||||||
|
|
||||||
use dioxus::{dioxus_core::CapturedError, prelude::*};
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
launch_desktop(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app() -> Element {
|
|
||||||
rsx! {
|
|
||||||
ErrorBoundary {
|
|
||||||
handle_error: |error: CapturedError| rsx! {
|
|
||||||
h1 { "An error occurred" }
|
|
||||||
pre { "{error:#?}" }
|
|
||||||
},
|
|
||||||
DemoC { x: 1 }
|
|
||||||
}
|
|
||||||
|
|
||||||
ErrorBoundary {
|
|
||||||
handle_error: |error: CapturedError| rsx! {
|
|
||||||
h1 { "Another error occurred" }
|
|
||||||
pre { "{error:#?}" }
|
|
||||||
},
|
|
||||||
ComponentPanic {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn DemoC(x: i32) -> Element {
|
|
||||||
rsx! {
|
|
||||||
h1 { "Error handler demo" }
|
|
||||||
button {
|
|
||||||
onclick: move |_| {
|
|
||||||
// Create an error
|
|
||||||
let result: Result<Element, &str> = Err("Error");
|
|
||||||
|
|
||||||
// And then call `throw` on it. The `throw` method is given by the `Throw` trait which is automatically
|
|
||||||
// imported via the prelude.
|
|
||||||
_ = result.throw();
|
|
||||||
},
|
|
||||||
"Click to throw an error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
fn ComponentPanic() -> Element {
|
|
||||||
panic!("This component panics")
|
|
||||||
}
|
|
161
examples/errors.rs
Normal file
161
examples/errors.rs
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
//! This example showcases how to use the ErrorBoundary component to handle errors in your app.
|
||||||
|
//!
|
||||||
|
//! The ErrorBoundary component is a special component that can be used to catch panics and other errors that occur.
|
||||||
|
//! By default, Dioxus will catch panics during rendering, async, and handlers, and bubble them up to the nearest
|
||||||
|
//! error boundary. If no error boundary is present, it will be caught by the root error boundary and the app will
|
||||||
|
//! render the error message as just a string.
|
||||||
|
//!
|
||||||
|
//! NOTE: In wasm, panics can currently not be caught by the error boundary. This is a limitation of WASM in rust.
|
||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
launch(|| rsx! { Router::<Route> {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// You can use an ErrorBoundary to catch errors in children and display a warning
|
||||||
|
fn Simple() -> Element {
|
||||||
|
rsx! {
|
||||||
|
GoBackButton { "Home" }
|
||||||
|
ErrorBoundary {
|
||||||
|
handle_error: |error: ErrorContext| rsx! {
|
||||||
|
h1 { "An error occurred" }
|
||||||
|
pre { "{error:#?}" }
|
||||||
|
},
|
||||||
|
ParseNumber {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ParseNumber() -> Element {
|
||||||
|
rsx! {
|
||||||
|
h1 { "Error handler demo" }
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
// You can return a result from an event handler which lets you easily quit rendering early if something fails
|
||||||
|
let data: i32 = "0.5".parse()?;
|
||||||
|
|
||||||
|
println!("parsed {data}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
"Click to throw an error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// You can provide additional context for the Error boundary to visualize
|
||||||
|
fn Show() -> Element {
|
||||||
|
rsx! {
|
||||||
|
GoBackButton { "Home" }
|
||||||
|
div {
|
||||||
|
ErrorBoundary {
|
||||||
|
handle_error: |errors: ErrorContext| {
|
||||||
|
rsx! {
|
||||||
|
for error in errors.errors() {
|
||||||
|
if let Some(error) = error.show() {
|
||||||
|
{error}
|
||||||
|
} else {
|
||||||
|
pre {
|
||||||
|
color: "red",
|
||||||
|
"{error}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ParseNumberWithShow {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ParseNumberWithShow() -> Element {
|
||||||
|
rsx! {
|
||||||
|
h1 { "Error handler demo" }
|
||||||
|
button {
|
||||||
|
onclick: move |_| {
|
||||||
|
let request_data = "0.5";
|
||||||
|
let data: i32 = request_data.parse()
|
||||||
|
// You can attach rsx to results that can be displayed in the Error Boundary
|
||||||
|
.show(|_| rsx!{
|
||||||
|
div {
|
||||||
|
background_color: "red",
|
||||||
|
border: "black",
|
||||||
|
border_width: "2px",
|
||||||
|
border_radius: "5px",
|
||||||
|
p { "Failed to parse data" }
|
||||||
|
Link {
|
||||||
|
to: Route::Home {},
|
||||||
|
"Go back to the homepage"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
println!("parsed {data}");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
"Click to throw an error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// On desktop, dioxus will catch panics in components and insert an error automatically
|
||||||
|
fn Panic() -> Element {
|
||||||
|
rsx! {
|
||||||
|
GoBackButton { "Home" }
|
||||||
|
ErrorBoundary {
|
||||||
|
handle_error: |errors: ErrorContext| rsx! {
|
||||||
|
h1 { "Another error occurred" }
|
||||||
|
pre { "{errors:#?}" }
|
||||||
|
},
|
||||||
|
ComponentPanic {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ComponentPanic() -> Element {
|
||||||
|
panic!("This component panics")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Routable, Clone, Debug, PartialEq)]
|
||||||
|
enum Route {
|
||||||
|
#[route("/")]
|
||||||
|
Home {},
|
||||||
|
#[route("/simple")]
|
||||||
|
Simple {},
|
||||||
|
#[route("/panic")]
|
||||||
|
Panic {},
|
||||||
|
#[route("/show")]
|
||||||
|
Show {},
|
||||||
|
}
|
||||||
|
|
||||||
|
fn Home() -> Element {
|
||||||
|
rsx! {
|
||||||
|
ul {
|
||||||
|
li {
|
||||||
|
Link {
|
||||||
|
to: Route::Simple {},
|
||||||
|
"Simple errors"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
Link {
|
||||||
|
to: Route::Panic {},
|
||||||
|
"Capture panics"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
Link {
|
||||||
|
to: Route::Show {},
|
||||||
|
"Show errors"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -170,13 +170,13 @@ fn app() -> Element {
|
||||||
|
|
||||||
// Can pass in props directly as an expression
|
// Can pass in props directly as an expression
|
||||||
{
|
{
|
||||||
let props = TallerProps {a: "hello", children: None };
|
let props = TallerProps {a: "hello", children: VNode::empty() };
|
||||||
rsx!(Taller { ..props })
|
rsx!(Taller { ..props })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Spreading can also be overridden manually
|
// Spreading can also be overridden manually
|
||||||
Taller {
|
Taller {
|
||||||
..TallerProps { a: "ballin!", children: None },
|
..TallerProps { a: "ballin!", children: VNode::empty() },
|
||||||
a: "not ballin!"
|
a: "not ballin!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,8 +193,8 @@ fn app() -> Element {
|
||||||
// Type inference can be used too
|
// Type inference can be used too
|
||||||
TypedInput { initial: 10.0 }
|
TypedInput { initial: 10.0 }
|
||||||
|
|
||||||
// geneircs with the `inline_props` macro
|
// generic with the `inline_props` macro
|
||||||
Label { text: "hello geneirc world!" }
|
Label { text: "hello generic world!" }
|
||||||
Label { text: 99.9 }
|
Label { text: 99.9 }
|
||||||
|
|
||||||
// Lowercase components work too, as long as they are access using a path
|
// Lowercase components work too, as long as they are access using a path
|
||||||
|
@ -283,7 +283,7 @@ where
|
||||||
return rsx! { "{props}" };
|
return rsx! { "{props}" };
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
VNode::empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
|
|
|
@ -22,7 +22,7 @@ fn app() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class, id, {&children} }
|
div { class, id, {&children} }
|
||||||
Component { a, b, c, children, onclick }
|
Component { a, b, c, children, onclick }
|
||||||
Component { a, ..ComponentProps { a: 1, b: 2, c: 3, children: None, onclick: Default::default() } }
|
Component { a, ..ComponentProps { a: 1, b: 2, c: 3, children: VNode::empty(), onclick: Default::default() } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -39,7 +39,14 @@ fn app() -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 { "Illustrious Dog Photo" }
|
h3 { "Illustrious Dog Photo" }
|
||||||
Doggo {}
|
SuspenseBoundary {
|
||||||
|
fallback: move |suspense: SuspenseContext| suspense.suspense_placeholder().unwrap_or_else(|| rsx! {
|
||||||
|
div {
|
||||||
|
"Loading..."
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
Doggo {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,7 +56,7 @@ fn app() -> Element {
|
||||||
/// actually renders the data.
|
/// actually renders the data.
|
||||||
#[component]
|
#[component]
|
||||||
fn Doggo() -> Element {
|
fn Doggo() -> Element {
|
||||||
let mut fut = use_resource(move || async move {
|
let mut resource = use_resource(move || async move {
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
struct DogApi {
|
struct DogApi {
|
||||||
message: String,
|
message: String,
|
||||||
|
@ -62,12 +69,26 @@ fn Doggo() -> Element {
|
||||||
.await
|
.await
|
||||||
});
|
});
|
||||||
|
|
||||||
match fut.read_unchecked().as_ref() {
|
// You can suspend the future and only continue rendering when it's ready
|
||||||
Some(Ok(resp)) => rsx! {
|
let value = resource.suspend().with_loading_placeholder(|| {
|
||||||
button { onclick: move |_| fut.restart(), "Click to fetch another doggo" }
|
rsx! {
|
||||||
|
div {
|
||||||
|
"Loading doggos..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match value.read_unchecked().as_ref() {
|
||||||
|
Ok(resp) => rsx! {
|
||||||
|
button { onclick: move |_| resource.restart(), "Click to fetch another doggo" }
|
||||||
div { img { max_width: "500px", max_height: "500px", src: "{resp.message}" } }
|
div { img { max_width: "500px", max_height: "500px", src: "{resp.message}" } }
|
||||||
},
|
},
|
||||||
Some(Err(_)) => rsx! { div { "loading dogs failed" } },
|
Err(_) => rsx! {
|
||||||
None => rsx! { div { "loading dogs..." } },
|
div { "loading dogs failed" }
|
||||||
|
button {
|
||||||
|
onclick: move |_| resource.restart(),
|
||||||
|
"retry"
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,10 +17,21 @@ pub use serve::*;
|
||||||
pub mod __private {
|
pub mod __private {
|
||||||
use crate::CrateConfig;
|
use crate::CrateConfig;
|
||||||
|
|
||||||
pub const CONFIG_ENV: &str = "DIOXUS_CONFIG";
|
pub(crate) const CONFIG_ENV: &str = "DIOXUS_CONFIG";
|
||||||
|
pub(crate) const CONFIG_BASE_PATH_ENV: &str = "DIOXUS_CONFIG_BASE_PATH";
|
||||||
|
|
||||||
pub fn save_config(config: &CrateConfig) -> CrateConfigDropGuard {
|
pub fn save_config(config: &CrateConfig) -> CrateConfigDropGuard {
|
||||||
std::env::set_var(CONFIG_ENV, serde_json::to_string(config).unwrap());
|
std::env::set_var(CONFIG_ENV, serde_json::to_string(config).unwrap());
|
||||||
|
std::env::set_var(
|
||||||
|
CONFIG_BASE_PATH_ENV,
|
||||||
|
config
|
||||||
|
.dioxus_config
|
||||||
|
.web
|
||||||
|
.app
|
||||||
|
.base_path
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
);
|
||||||
CrateConfigDropGuard
|
CrateConfigDropGuard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +41,7 @@ pub mod __private {
|
||||||
impl Drop for CrateConfigDropGuard {
|
impl Drop for CrateConfigDropGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
std::env::remove_var(CONFIG_ENV);
|
std::env::remove_var(CONFIG_ENV);
|
||||||
|
std::env::remove_var(CONFIG_BASE_PATH_ENV);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,3 +79,7 @@ pub static CURRENT_CONFIG: once_cell::sync::Lazy<
|
||||||
#[cfg(feature = "read-config")]
|
#[cfg(feature = "read-config")]
|
||||||
/// The current crate's configuration.
|
/// The current crate's configuration.
|
||||||
pub const CURRENT_CONFIG_JSON: Option<&str> = std::option_env!("DIOXUS_CONFIG");
|
pub const CURRENT_CONFIG_JSON: Option<&str> = std::option_env!("DIOXUS_CONFIG");
|
||||||
|
|
||||||
|
#[cfg(feature = "read-config")]
|
||||||
|
/// The current crate's configuration.
|
||||||
|
pub const BASE_PATH: Option<&str> = std::option_env!("DIOXUS_CONFIG_BASE_PATH");
|
||||||
|
|
|
@ -105,13 +105,16 @@ fn copy_dir_to(src_dir: PathBuf, dest_dir: PathBuf, pre_compress: bool) -> std::
|
||||||
|
|
||||||
// Then pre-compress the file if needed
|
// Then pre-compress the file if needed
|
||||||
if pre_compress {
|
if pre_compress {
|
||||||
if let Err(err) = pre_compress_file(&entry_path.clone()) {
|
if let Err(err) = pre_compress_file(&output_file_location) {
|
||||||
tracing::error!(
|
tracing::error!(
|
||||||
"Failed to pre-compress static assets {}: {}",
|
"Failed to pre-compress static assets {}: {}",
|
||||||
entry_path.display(),
|
output_file_location.display(),
|
||||||
err
|
err
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// If pre-compression isn't enabled, we should remove the old compressed file if it exists
|
||||||
|
} else if let Some(compressed_path) = compressed_path(&output_file_location) {
|
||||||
|
_ = std::fs::remove_file(compressed_path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,12 +127,12 @@ fn copy_dir_to(src_dir: PathBuf, dest_dir: PathBuf, pre_compress: bool) -> std::
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// pre-compress a file with brotli
|
/// Get the path to the compressed version of a file
|
||||||
pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> {
|
fn compressed_path(path: &Path) -> Option<PathBuf> {
|
||||||
let new_extension = match path.extension() {
|
let new_extension = match path.extension() {
|
||||||
Some(ext) => {
|
Some(ext) => {
|
||||||
if ext.to_string_lossy().to_lowercase().ends_with("br") {
|
if ext.to_string_lossy().to_lowercase().ends_with("br") {
|
||||||
return Ok(());
|
return None;
|
||||||
}
|
}
|
||||||
let mut ext = ext.to_os_string();
|
let mut ext = ext.to_os_string();
|
||||||
ext.push(".br");
|
ext.push(".br");
|
||||||
|
@ -137,22 +140,37 @@ pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> {
|
||||||
}
|
}
|
||||||
None => OsString::from("br"),
|
None => OsString::from("br"),
|
||||||
};
|
};
|
||||||
|
Some(path.with_extension(new_extension))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// pre-compress a file with brotli
|
||||||
|
pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> {
|
||||||
|
let Some(compressed_path) = compressed_path(path) else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
let file = std::fs::File::open(path)?;
|
let file = std::fs::File::open(path)?;
|
||||||
let mut stream = std::io::BufReader::new(file);
|
let mut stream = std::io::BufReader::new(file);
|
||||||
let output = path.with_extension(new_extension);
|
let mut buffer = std::fs::File::create(compressed_path)?;
|
||||||
let mut buffer = std::fs::File::create(output)?;
|
|
||||||
let params = BrotliEncoderParams::default();
|
let params = BrotliEncoderParams::default();
|
||||||
brotli::BrotliCompress(&mut stream, &mut buffer, ¶ms)?;
|
brotli::BrotliCompress(&mut stream, &mut buffer, ¶ms)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// pre-compress all files in a folder
|
/// pre-compress all files in a folder
|
||||||
pub(crate) fn pre_compress_folder(path: &Path) -> std::io::Result<()> {
|
pub(crate) fn pre_compress_folder(path: &Path, pre_compress: bool) -> std::io::Result<()> {
|
||||||
let walk_dir = WalkDir::new(path);
|
let walk_dir = WalkDir::new(path);
|
||||||
for entry in walk_dir.into_iter().filter_map(|e| e.ok()) {
|
for entry in walk_dir.into_iter().filter_map(|e| e.ok()) {
|
||||||
let entry_path = entry.path();
|
let entry_path = entry.path();
|
||||||
if entry_path.is_file() {
|
if entry_path.is_file() {
|
||||||
pre_compress_file(entry_path)?;
|
if pre_compress {
|
||||||
|
if let Err(err) = pre_compress_file(entry_path) {
|
||||||
|
tracing::error!("Failed to pre-compress file {entry_path:?}: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If pre-compression isn't enabled, we should remove the old compressed file if it exists
|
||||||
|
else if let Some(compressed_path) = compressed_path(entry_path) {
|
||||||
|
_ = std::fs::remove_file(compressed_path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,22 +1,38 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>{app_title}</title>
|
<title>{app_title}</title>
|
||||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
{style_include}
|
<link
|
||||||
</head>
|
rel="preload"
|
||||||
<body>
|
href="/{base_path}/assets/dioxus/{app_name}.js"
|
||||||
<div id="main"></div>
|
as="style"
|
||||||
<script type="module">
|
/>
|
||||||
import init from "/{base_path}/assets/dioxus/{app_name}.js";
|
<link
|
||||||
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then(wasm => {
|
rel="preload"
|
||||||
if (wasm.__wbindgen_start == undefined) {
|
href="/{base_path}/assets/dioxus/{app_name}_bg.wasm"
|
||||||
wasm.main();
|
as="fetch"
|
||||||
}
|
type="application/wasm"
|
||||||
});
|
crossorigin=""
|
||||||
</script>
|
/>
|
||||||
{script_include}
|
{style_include}
|
||||||
</body>
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="main"></div>
|
||||||
|
<script>
|
||||||
|
// We can't use a module script here because we need to start the script immediately when streaming
|
||||||
|
import("/{base_path}/assets/dioxus/{app_name}.js").then(
|
||||||
|
({ default: init }) => {
|
||||||
|
init("/{base_path}/assets/dioxus/{app_name}_bg.wasm").then((wasm) => {
|
||||||
|
if (wasm.__wbindgen_start == undefined) {
|
||||||
|
wasm.main();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
{script_include}
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -230,9 +230,7 @@ pub fn build_web(
|
||||||
}
|
}
|
||||||
|
|
||||||
// If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
|
// If pre-compressing is enabled, we can pre_compress the wasm-bindgen output
|
||||||
if config.should_pre_compress_web_assets() {
|
pre_compress_folder(&bindgen_outdir, config.should_pre_compress_web_assets())?;
|
||||||
pre_compress_folder(&bindgen_outdir)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
|
// [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS
|
||||||
let dioxus_tools = dioxus_config.application.tools.clone();
|
let dioxus_tools = dioxus_config.application.tools.clone();
|
||||||
|
@ -624,6 +622,16 @@ pub fn gen_page(config: &CrateConfig, manifest: Option<&AssetManifest>, serve: b
|
||||||
</body"#
|
</body"#
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// And try to insert preload links for the wasm and js files
|
||||||
|
html = html.replace(
|
||||||
|
"</head",
|
||||||
|
&format!(
|
||||||
|
r#"<link rel="preload" href="/{base_path}/assets/dioxus/{app_name}_bg.wasm" as="fetch" type="application/wasm" crossorigin="">
|
||||||
|
<link rel="preload" href="/{base_path}/assets/dioxus/{app_name}.js" as="script">
|
||||||
|
</head"#
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = config.dioxus_config.web.app.title.clone();
|
let title = config.dioxus_config.web.app.title.clone();
|
||||||
|
|
|
@ -16,6 +16,11 @@ pub struct ConfigOptsBuild {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub force_debug: bool,
|
pub force_debug: bool,
|
||||||
|
|
||||||
|
/// This flag only applies to fullstack builds. By default fullstack builds will run the server and client builds in parallel. This flag will force the build to run the server build first, then the client build. [default: false]
|
||||||
|
#[clap(long)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub force_sequential: bool,
|
||||||
|
|
||||||
// Use verbose output [default: false]
|
// Use verbose output [default: false]
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -73,6 +78,7 @@ impl From<ConfigOptsServe> for ConfigOptsBuild {
|
||||||
server_feature: serve.server_feature,
|
server_feature: serve.server_feature,
|
||||||
skip_assets: serve.skip_assets,
|
skip_assets: serve.skip_assets,
|
||||||
force_debug: serve.force_debug,
|
force_debug: serve.force_debug,
|
||||||
|
force_sequential: serve.force_sequential,
|
||||||
cargo_args: serve.cargo_args,
|
cargo_args: serve.cargo_args,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,6 +110,11 @@ pub struct ConfigOptsServe {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub force_debug: bool,
|
pub force_debug: bool,
|
||||||
|
|
||||||
|
/// This flag only applies to fullstack builds. By default fullstack builds will run the server and client builds in parallel. This flag will force the build to run the server build first, then the client build. [default: false]
|
||||||
|
#[clap(long)]
|
||||||
|
#[serde(default)]
|
||||||
|
pub force_sequential: bool,
|
||||||
|
|
||||||
// Use verbose output [default: false]
|
// Use verbose output [default: false]
|
||||||
#[clap(long)]
|
#[clap(long)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
@ -240,17 +240,11 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn start_desktop(
|
fn run_desktop(
|
||||||
config: &CrateConfig,
|
|
||||||
skip_assets: bool,
|
|
||||||
rust_flags: Option<String>,
|
|
||||||
args: &Vec<String>,
|
args: &Vec<String>,
|
||||||
env: Vec<(String, String)>,
|
env: Vec<(String, String)>,
|
||||||
|
result: BuildResult,
|
||||||
) -> Result<(RAIIChild, BuildResult)> {
|
) -> Result<(RAIIChild, BuildResult)> {
|
||||||
// Run the desktop application
|
|
||||||
// Only used for the fullstack platform,
|
|
||||||
let result = crate::builder::build_desktop(config, true, skip_assets, rust_flags)?;
|
|
||||||
|
|
||||||
let active = "DIOXUS_ACTIVE";
|
let active = "DIOXUS_ACTIVE";
|
||||||
let child = RAIIChild(
|
let child = RAIIChild(
|
||||||
Command::new(
|
Command::new(
|
||||||
|
@ -278,13 +272,12 @@ impl DesktopPlatform {
|
||||||
/// `rust_flags` argument is added because it is used by the
|
/// `rust_flags` argument is added because it is used by the
|
||||||
/// `DesktopPlatform`'s implementation of the `Platform::start()`.
|
/// `DesktopPlatform`'s implementation of the `Platform::start()`.
|
||||||
pub fn start_with_options(
|
pub fn start_with_options(
|
||||||
|
build_result: BuildResult,
|
||||||
config: &CrateConfig,
|
config: &CrateConfig,
|
||||||
serve: &ConfigOptsServe,
|
serve: &ConfigOptsServe,
|
||||||
rust_flags: Option<String>,
|
|
||||||
env: Vec<(String, String)>,
|
env: Vec<(String, String)>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let (child, first_build_result) =
|
let (child, first_build_result) = run_desktop(&serve.args, env, build_result)?;
|
||||||
start_desktop(config, serve.skip_assets, rust_flags, &serve.args, env)?;
|
|
||||||
|
|
||||||
tracing::info!("🚀 Starting development server...");
|
tracing::info!("🚀 Starting development server...");
|
||||||
|
|
||||||
|
@ -337,7 +330,9 @@ impl DesktopPlatform {
|
||||||
// Todo: add a timeout here to kill the process if it doesn't shut down within a reasonable time
|
// Todo: add a timeout here to kill the process if it doesn't shut down within a reasonable time
|
||||||
self.currently_running_child.0.wait()?;
|
self.currently_running_child.0.wait()?;
|
||||||
|
|
||||||
let (child, result) = start_desktop(config, self.skip_assets, rust_flags, &self.args, env)?;
|
let build_result =
|
||||||
|
crate::builder::build_desktop(config, true, self.skip_assets, rust_flags)?;
|
||||||
|
let (child, result) = run_desktop(&self.args, env, build_result)?;
|
||||||
self.currently_running_child = child;
|
self.currently_running_child = child;
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
@ -349,11 +344,8 @@ impl Platform for DesktopPlatform {
|
||||||
serve: &ConfigOptsServe,
|
serve: &ConfigOptsServe,
|
||||||
env: Vec<(String, String)>,
|
env: Vec<(String, String)>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
// See `start_with_options()`'s docs for the explanation why the code
|
let build_result = crate::builder::build_desktop(config, true, serve.skip_assets, None)?;
|
||||||
// was moved there.
|
DesktopPlatform::start_with_options(build_result, config, serve, env)
|
||||||
// Since desktop platform doesn't use `rust_flags`, this argument is
|
|
||||||
// explicitly set to `None`.
|
|
||||||
DesktopPlatform::start_with_options(config, serve, None, env)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rebuild(
|
fn rebuild(
|
||||||
|
|
|
@ -5,7 +5,10 @@ use crate::{
|
||||||
BuildResult, Result,
|
BuildResult, Result,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{desktop, Platform};
|
use super::{
|
||||||
|
desktop::{self, DesktopPlatform},
|
||||||
|
Platform,
|
||||||
|
};
|
||||||
|
|
||||||
static CLIENT_RUST_FLAGS: &str = "-C debuginfo=none -C strip=debuginfo";
|
static CLIENT_RUST_FLAGS: &str = "-C debuginfo=none -C strip=debuginfo";
|
||||||
// The `opt-level=2` increases build times, but can noticeably decrease time
|
// The `opt-level=2` increases build times, but can noticeably decrease time
|
||||||
|
@ -49,7 +52,9 @@ fn start_web_build_thread(
|
||||||
|
|
||||||
fn make_desktop_config(config: &CrateConfig, serve: &ConfigOptsServe) -> CrateConfig {
|
fn make_desktop_config(config: &CrateConfig, serve: &ConfigOptsServe) -> CrateConfig {
|
||||||
let mut desktop_config = config.clone();
|
let mut desktop_config = config.clone();
|
||||||
desktop_config.target_dir = config.server_target_dir();
|
if !serve.force_sequential {
|
||||||
|
desktop_config.target_dir = config.server_target_dir();
|
||||||
|
}
|
||||||
let desktop_feature = serve.server_feature.clone();
|
let desktop_feature = serve.server_feature.clone();
|
||||||
let features = &mut desktop_config.features;
|
let features = &mut desktop_config.features;
|
||||||
match features {
|
match features {
|
||||||
|
@ -89,16 +94,20 @@ impl Platform for FullstackPlatform {
|
||||||
let server_rust_flags = server_rust_flags(&serve.clone().into());
|
let server_rust_flags = server_rust_flags(&serve.clone().into());
|
||||||
let mut desktop_env = env.clone();
|
let mut desktop_env = env.clone();
|
||||||
add_serve_options_to_env(serve, &mut desktop_env);
|
add_serve_options_to_env(serve, &mut desktop_env);
|
||||||
let desktop = desktop::DesktopPlatform::start_with_options(
|
let build_result = crate::builder::build_desktop(
|
||||||
&desktop_config,
|
&desktop_config,
|
||||||
serve,
|
true,
|
||||||
|
serve.skip_assets,
|
||||||
Some(server_rust_flags.clone()),
|
Some(server_rust_flags.clone()),
|
||||||
desktop_env,
|
|
||||||
)?;
|
)?;
|
||||||
thread_handle
|
thread_handle
|
||||||
.join()
|
.join()
|
||||||
.map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
|
.map_err(|_| anyhow::anyhow!("Failed to join thread"))??;
|
||||||
|
|
||||||
|
// Only start the server after the web build is finished
|
||||||
|
let desktop =
|
||||||
|
DesktopPlatform::start_with_options(build_result, &desktop_config, serve, desktop_env)?;
|
||||||
|
|
||||||
if serve.open {
|
if serve.open {
|
||||||
crate::server::web::open_browser(
|
crate::server::web::open_browser(
|
||||||
config,
|
config,
|
||||||
|
@ -157,7 +166,7 @@ fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> Resu
|
||||||
}
|
}
|
||||||
.build(
|
.build(
|
||||||
None,
|
None,
|
||||||
Some(target_directory),
|
(!web_config.force_sequential).then_some(target_directory),
|
||||||
Some(client_rust_flags(&web_config)),
|
Some(client_rust_flags(&web_config)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -201,9 +201,8 @@ mod field_info {
|
||||||
|
|
||||||
// children field is automatically defaulted to None
|
// children field is automatically defaulted to None
|
||||||
if name == "children" {
|
if name == "children" {
|
||||||
builder_attr.default = Some(
|
builder_attr.default =
|
||||||
syn::parse(quote!(::core::default::Default::default()).into()).unwrap(),
|
Some(syn::parse(quote!(dioxus_core::VNode::empty()).into()).unwrap());
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// String fields automatically use impl Display
|
// String fields automatically use impl Display
|
||||||
|
@ -1046,7 +1045,6 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
ty: field_type,
|
ty: field_type,
|
||||||
..
|
..
|
||||||
} = field;
|
} = field;
|
||||||
// Add the bump lifetime to the generics
|
|
||||||
let mut ty_generics: Vec<syn::GenericArgument> = self
|
let mut ty_generics: Vec<syn::GenericArgument> = self
|
||||||
.generics
|
.generics
|
||||||
.params
|
.params
|
||||||
|
@ -1198,7 +1196,6 @@ Finally, call `.build()` to create the instance of `{name}`.
|
||||||
name: ref field_name,
|
name: ref field_name,
|
||||||
..
|
..
|
||||||
} = field;
|
} = field;
|
||||||
// Add a bump lifetime to the generics
|
|
||||||
let mut builder_generics: Vec<syn::GenericArgument> = self
|
let mut builder_generics: Vec<syn::GenericArgument> = self
|
||||||
.generics
|
.generics
|
||||||
.params
|
.params
|
||||||
|
|
|
@ -23,7 +23,7 @@ async fn values_memoize_in_place() {
|
||||||
use_hook(|| {
|
use_hook(|| {
|
||||||
spawn(async move {
|
spawn(async move {
|
||||||
for _ in 0..15 {
|
for _ in 0..15 {
|
||||||
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
tokio::time::sleep(std::time::Duration::from_millis(1)).await;
|
||||||
count += 1;
|
count += 1;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -36,8 +36,9 @@ async fn values_memoize_in_place() {
|
||||||
let _ = &x;
|
let _ = &x;
|
||||||
println!("num is {num}");
|
println!("num is {num}");
|
||||||
},
|
},
|
||||||
children: count() / 2
|
number: count() / 2
|
||||||
}
|
}
|
||||||
|
TakesSignal { sig: count(), number: count() / 2 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,8 +47,8 @@ async fn values_memoize_in_place() {
|
||||||
|
|
||||||
let mutations = dom.rebuild_to_vec();
|
let mutations = dom.rebuild_to_vec();
|
||||||
println!("{:#?}", mutations);
|
println!("{:#?}", mutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
for _ in 0..20 {
|
for _ in 0..40 {
|
||||||
dom.handle_event(
|
dom.handle_event(
|
||||||
"click",
|
"click",
|
||||||
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
|
Rc::new(PlatformEventData::new(Box::<SerializedMouseData>::default())),
|
||||||
|
@ -74,7 +75,8 @@ fn cloning_event_handler_components_work() {
|
||||||
TakesEventHandler {
|
TakesEventHandler {
|
||||||
click: move |evt| {
|
click: move |evt| {
|
||||||
println!("Clicked {evt:?}!");
|
println!("Clicked {evt:?}!");
|
||||||
}
|
},
|
||||||
|
number: 0
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -91,7 +93,7 @@ fn cloning_event_handler_components_work() {
|
||||||
|
|
||||||
let mutations = dom.rebuild_to_vec();
|
let mutations = dom.rebuild_to_vec();
|
||||||
println!("{:#?}", mutations);
|
println!("{:#?}", mutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
for _ in 0..20 {
|
for _ in 0..20 {
|
||||||
dom.handle_event(
|
dom.handle_event(
|
||||||
"click",
|
"click",
|
||||||
|
@ -105,24 +107,23 @@ fn cloning_event_handler_components_work() {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn TakesEventHandler(click: EventHandler<usize>, children: usize) -> Element {
|
fn TakesEventHandler(click: EventHandler<usize>, number: usize) -> Element {
|
||||||
println!("children is{children}");
|
|
||||||
let first_render_click = use_hook(move || click);
|
let first_render_click = use_hook(move || click);
|
||||||
if generation() > 0 {
|
if generation() > 0 {
|
||||||
// Make sure the event handler is memoized in place and never gets dropped
|
// Make sure the event handler is memoized in place and never gets dropped
|
||||||
first_render_click(children);
|
first_render_click(number);
|
||||||
}
|
}
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
button {
|
button {
|
||||||
onclick: move |_| click(children),
|
onclick: move |_| click(number),
|
||||||
"{children}"
|
"{number}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn TakesSignal(sig: ReadOnlySignal<usize>, children: usize) -> Element {
|
fn TakesSignal(sig: ReadOnlySignal<usize>, number: usize) -> Element {
|
||||||
let first_render_sig = use_hook(move || sig);
|
let first_render_sig = use_hook(move || sig);
|
||||||
if generation() > 0 {
|
if generation() > 0 {
|
||||||
// Make sure the signal is memoized in place and never gets dropped
|
// Make sure the signal is memoized in place and never gets dropped
|
||||||
|
@ -130,6 +131,6 @@ fn TakesSignal(sig: ReadOnlySignal<usize>, children: usize) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
button { "{children}" }
|
button { "{number}" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ slotmap = { workspace = true }
|
||||||
futures-channel = { workspace = true }
|
futures-channel = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
tracing-subscriber = "0.3.18"
|
|
||||||
generational-box = { workspace = true }
|
generational-box = { workspace = true }
|
||||||
rustversion = "1.0.17"
|
rustversion = "1.0.17"
|
||||||
|
|
||||||
|
@ -29,20 +28,22 @@ rustversion = "1.0.17"
|
||||||
tokio = { workspace = true, features = ["full"] }
|
tokio = { workspace = true, features = ["full"] }
|
||||||
tracing-fluent-assertions = "0.3.0"
|
tracing-fluent-assertions = "0.3.0"
|
||||||
dioxus = { workspace = true }
|
dioxus = { workspace = true }
|
||||||
|
dioxus-html = { workspace = true, features = ["serialize"] }
|
||||||
pretty_assertions = "1.3.0"
|
pretty_assertions = "1.3.0"
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
dioxus-ssr = { workspace = true }
|
dioxus-ssr = { workspace = true }
|
||||||
reqwest = { workspace = true}
|
reqwest = { workspace = true}
|
||||||
|
tracing-subscriber = "0.3.18"
|
||||||
|
|
||||||
[dev-dependencies.web-sys]
|
[dev-dependencies.web-sys]
|
||||||
version = "0.3.56"
|
version = "0.3.56"
|
||||||
features = [
|
features = [
|
||||||
"Document",
|
"Document",
|
||||||
"HtmlElement",
|
"HtmlElement",
|
||||||
|
"Window"
|
||||||
]
|
]
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
|
||||||
serialize = ["dep:serde"]
|
serialize = ["dep:serde"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|
|
@ -18,7 +18,7 @@ loop {
|
||||||
vdom.render_immediate(&mut real_dom.apply())
|
vdom.render_immediate(&mut real_dom.apply())
|
||||||
}
|
}
|
||||||
|
|
||||||
# fn app() -> Element { None }
|
# fn app() -> Element { VNode::empty() }
|
||||||
# struct SomeRenderer; impl SomeRenderer { fn new() -> SomeRenderer { SomeRenderer } async fn event(&self) -> std::rc::Rc<dyn std::any::Any> { unimplemented!() } fn apply(&self) -> Mutations { Mutations::default() } }
|
# struct SomeRenderer; impl SomeRenderer { fn new() -> SomeRenderer { SomeRenderer } async fn event(&self) -> std::rc::Rc<dyn std::any::Any> { unimplemented!() } fn apply(&self) -> Mutations { Mutations::default() } }
|
||||||
# });
|
# });
|
||||||
```
|
```
|
||||||
|
|
|
@ -1,8 +1,4 @@
|
||||||
use crate::{
|
use crate::{innerlude::CapturedPanic, nodes::RenderReturn, ComponentFunction};
|
||||||
innerlude::{throw_error, CapturedPanic},
|
|
||||||
nodes::RenderReturn,
|
|
||||||
ComponentFunction,
|
|
||||||
};
|
|
||||||
use std::{any::Any, panic::AssertUnwindSafe};
|
use std::{any::Any, panic::AssertUnwindSafe};
|
||||||
|
|
||||||
pub(crate) type BoxedAnyProps = Box<dyn AnyProps>;
|
pub(crate) type BoxedAnyProps = Box<dyn AnyProps>;
|
||||||
|
@ -15,6 +11,8 @@ pub(crate) trait AnyProps: 'static {
|
||||||
fn memoize(&mut self, other: &dyn Any) -> bool;
|
fn memoize(&mut self, other: &dyn Any) -> bool;
|
||||||
/// Get the props as a type erased `dyn Any`.
|
/// Get the props as a type erased `dyn Any`.
|
||||||
fn props(&self) -> &dyn Any;
|
fn props(&self) -> &dyn Any;
|
||||||
|
/// Get the props as a type erased `dyn Any`.
|
||||||
|
fn props_mut(&mut self) -> &mut dyn Any;
|
||||||
/// Duplicate this component into a new boxed component.
|
/// Duplicate this component into a new boxed component.
|
||||||
fn duplicate(&self) -> BoxedAnyProps;
|
fn duplicate(&self) -> BoxedAnyProps;
|
||||||
}
|
}
|
||||||
|
@ -72,19 +70,24 @@ impl<F: ComponentFunction<P, M> + Clone, P: Clone + 'static, M: 'static> AnyProp
|
||||||
&self.props
|
&self.props
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn props_mut(&mut self) -> &mut dyn Any {
|
||||||
|
&mut self.props
|
||||||
|
}
|
||||||
|
|
||||||
fn render(&self) -> RenderReturn {
|
fn render(&self) -> RenderReturn {
|
||||||
let res = std::panic::catch_unwind(AssertUnwindSafe(move || {
|
let res = std::panic::catch_unwind(AssertUnwindSafe(move || {
|
||||||
self.render_fn.rebuild(self.props.clone())
|
self.render_fn.rebuild(self.props.clone())
|
||||||
}));
|
}));
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
Ok(Some(e)) => RenderReturn::Ready(e),
|
Ok(node) => RenderReturn { node },
|
||||||
Ok(None) => RenderReturn::default(),
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let component_name = self.name;
|
let component_name = self.name;
|
||||||
tracing::error!("Error while rendering component `{component_name}`: {err:?}");
|
tracing::error!("Error while rendering component `{component_name}`: {err:?}");
|
||||||
throw_error::<()>(CapturedPanic { error: err });
|
let panic = CapturedPanic { error: err };
|
||||||
RenderReturn::default()
|
RenderReturn {
|
||||||
|
node: Err(panic.into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,18 +19,25 @@ pub(crate) struct MountId(pub(crate) usize);
|
||||||
|
|
||||||
impl Default for MountId {
|
impl Default for MountId {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(usize::MAX)
|
Self::PLACEHOLDER
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MountId {
|
impl MountId {
|
||||||
|
pub(crate) const PLACEHOLDER: Self = Self(usize::MAX);
|
||||||
|
|
||||||
pub(crate) fn as_usize(self) -> Option<usize> {
|
pub(crate) fn as_usize(self) -> Option<usize> {
|
||||||
if self.0 == usize::MAX {
|
if self == Self::PLACEHOLDER {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(self.0)
|
Some(self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn mounted(self) -> bool {
|
||||||
|
self != Self::PLACEHOLDER
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
@ -53,16 +60,18 @@ impl VirtualDom {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn reclaim(&mut self, el: ElementId) {
|
pub(crate) fn reclaim(&mut self, el: ElementId) {
|
||||||
self.try_reclaim(el)
|
if !self.try_reclaim(el) {
|
||||||
.unwrap_or_else(|| panic!("cannot reclaim {:?}", el));
|
tracing::error!("cannot reclaim {:?}", el);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> {
|
pub(crate) fn try_reclaim(&mut self, el: ElementId) -> bool {
|
||||||
if el.0 == 0 {
|
// We never reclaim the unmounted elements or the root element
|
||||||
panic!("Cannot reclaim the root element",);
|
if el.0 == 0 || el.0 == usize::MAX {
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.elements.try_remove(el.0).map(|_| ())
|
self.elements.try_remove(el.0).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop a scope without dropping its children
|
// Drop a scope without dropping its children
|
||||||
|
|
|
@ -1,56 +1,125 @@
|
||||||
use std::ops::{Deref, DerefMut};
|
use std::{
|
||||||
|
any::TypeId,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
any_props::AnyProps,
|
any_props::AnyProps,
|
||||||
innerlude::{ElementRef, MountId, ScopeOrder, VComponent, WriteMutations},
|
innerlude::{
|
||||||
nodes::RenderReturn,
|
ElementRef, MountId, ScopeOrder, SuspenseBoundaryProps, SuspenseBoundaryPropsWithOwner,
|
||||||
|
VComponent, WriteMutations,
|
||||||
|
},
|
||||||
nodes::VNode,
|
nodes::VNode,
|
||||||
scopes::ScopeId,
|
scopes::ScopeId,
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
|
RenderReturn,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
pub(crate) fn diff_scope(
|
pub(crate) fn run_and_diff_scope<M: WriteMutations>(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
to: Option<&mut M>,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
) {
|
||||||
|
let scope = &mut self.scopes[scope_id.0];
|
||||||
|
if SuspenseBoundaryProps::downcast_mut_from_props(&mut *scope.props).is_some() {
|
||||||
|
SuspenseBoundaryProps::diff(scope_id, self, to)
|
||||||
|
} else {
|
||||||
|
let new_nodes = self.run_scope(scope_id);
|
||||||
|
self.diff_scope(to, scope_id, new_nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self, to), level = "trace", name = "VirtualDom::diff_scope")]
|
||||||
|
fn diff_scope<M: WriteMutations>(
|
||||||
|
&mut self,
|
||||||
|
to: Option<&mut M>,
|
||||||
scope: ScopeId,
|
scope: ScopeId,
|
||||||
new_nodes: RenderReturn,
|
new_nodes: RenderReturn,
|
||||||
) {
|
) {
|
||||||
self.runtime.scope_stack.borrow_mut().push(scope);
|
// We don't diff the nodes if the scope is suspended or has an error
|
||||||
|
let Ok(new_real_nodes) = &new_nodes.node else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
self.runtime.push_scope(scope);
|
||||||
let scope_state = &mut self.scopes[scope.0];
|
let scope_state = &mut self.scopes[scope.0];
|
||||||
// Load the old and new bump arenas
|
// Load the old and new rendered nodes
|
||||||
let new = &new_nodes;
|
|
||||||
let old = scope_state.last_rendered_node.take().unwrap();
|
let old = scope_state.last_rendered_node.take().unwrap();
|
||||||
|
|
||||||
old.diff_node(new, self, to);
|
// If there are suspended scopes, we need to check if the scope is suspended before we diff it
|
||||||
|
// If it is suspended, we need to diff it but write the mutations nothing
|
||||||
|
// Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
|
||||||
|
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
|
||||||
|
old.diff_node(new_real_nodes, self, render_to.as_deref_mut());
|
||||||
|
|
||||||
let scope_state = &mut self.scopes[scope.0];
|
self.scopes[scope.0].last_rendered_node = Some(new_nodes);
|
||||||
scope_state.last_rendered_node = Some(new_nodes);
|
|
||||||
|
|
||||||
self.runtime.scope_stack.borrow_mut().pop();
|
if render_to.is_some() {
|
||||||
|
self.runtime.get_state(scope).unwrap().mount(&self.runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runtime.pop_scope();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new template [`VNode`] and write it to the [`Mutations`] buffer.
|
/// Create a new [`ScopeState`] for a component that has been created with [`VirtualDom::create_scope`]
|
||||||
///
|
///
|
||||||
/// This method pushes the ScopeID to the internal scopestack and returns the number of nodes created.
|
/// Returns the number of nodes created on the stack
|
||||||
pub(crate) fn create_scope(
|
#[tracing::instrument(skip(self, to), level = "trace", name = "VirtualDom::create_scope")]
|
||||||
|
pub(crate) fn create_scope<M: WriteMutations>(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
to: Option<&mut M>,
|
||||||
scope: ScopeId,
|
scope: ScopeId,
|
||||||
new_node: RenderReturn,
|
new_nodes: RenderReturn,
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
self.runtime.scope_stack.borrow_mut().push(scope);
|
self.runtime.push_scope(scope);
|
||||||
|
|
||||||
|
// If there are suspended scopes, we need to check if the scope is suspended before we diff it
|
||||||
|
// If it is suspended, we need to diff it but write the mutations nothing
|
||||||
|
// Note: It is important that we still diff the scope even if it is suspended, because the scope may render other child components which may change between renders
|
||||||
|
let mut render_to = to.filter(|_| self.runtime.scope_should_render(scope));
|
||||||
|
|
||||||
// Create the node
|
// Create the node
|
||||||
let nodes = new_node.create(self, to, parent);
|
let nodes = new_nodes.create(self, parent, render_to.as_deref_mut());
|
||||||
|
|
||||||
// Then set the new node as the last rendered node
|
// Then set the new node as the last rendered node
|
||||||
self.scopes[scope.0].last_rendered_node = Some(new_node);
|
self.scopes[scope.0].last_rendered_node = Some(new_nodes);
|
||||||
|
|
||||||
self.runtime.scope_stack.borrow_mut().pop();
|
if render_to.is_some() {
|
||||||
|
self.runtime.get_state(scope).unwrap().mount(&self.runtime);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.runtime.pop_scope();
|
||||||
nodes
|
nodes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_component_node<M: WriteMutations>(
|
||||||
|
&mut self,
|
||||||
|
to: Option<&mut M>,
|
||||||
|
destroy_component_state: bool,
|
||||||
|
scope_id: ScopeId,
|
||||||
|
replace_with: Option<usize>,
|
||||||
|
) {
|
||||||
|
// If this is a suspense boundary, remove the suspended nodes as well
|
||||||
|
if let Some(mut suspense) =
|
||||||
|
SuspenseBoundaryProps::downcast_mut_from_props(&mut *self.scopes[scope_id.0].props)
|
||||||
|
.cloned()
|
||||||
|
{
|
||||||
|
suspense.remove_suspended_nodes::<M>(self, destroy_component_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the component from the dom
|
||||||
|
if let Some(node) = self.scopes[scope_id.0].last_rendered_node.as_ref() {
|
||||||
|
node.clone_mounted()
|
||||||
|
.remove_node_inner(self, to, destroy_component_state, replace_with)
|
||||||
|
};
|
||||||
|
|
||||||
|
if destroy_component_state {
|
||||||
|
// Now drop all the resources
|
||||||
|
self.drop_scope(scope_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VNode {
|
impl VNode {
|
||||||
|
@ -63,7 +132,7 @@ impl VNode {
|
||||||
scope_id: ScopeId,
|
scope_id: ScopeId,
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
dom: &mut VirtualDom,
|
dom: &mut VirtualDom,
|
||||||
to: &mut impl WriteMutations,
|
to: Option<&mut impl WriteMutations>,
|
||||||
) {
|
) {
|
||||||
// Replace components that have different render fns
|
// Replace components that have different render fns
|
||||||
if old.render_fn != new.render_fn {
|
if old.render_fn != new.render_fn {
|
||||||
|
@ -83,9 +152,8 @@ impl VNode {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now run the component and diff it
|
// Now diff the scope
|
||||||
let new = dom.run_scope(scope_id);
|
dom.run_and_diff_scope(to, scope_id);
|
||||||
dom.diff_scope(to, scope_id, new);
|
|
||||||
|
|
||||||
let height = dom.runtime.get_state(scope_id).unwrap().height;
|
let height = dom.runtime.get_state(scope_id).unwrap().height;
|
||||||
dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id));
|
dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id));
|
||||||
|
@ -98,16 +166,21 @@ impl VNode {
|
||||||
new: &VComponent,
|
new: &VComponent,
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
dom: &mut VirtualDom,
|
dom: &mut VirtualDom,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
) {
|
) {
|
||||||
let scope = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]);
|
let scope = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]);
|
||||||
|
|
||||||
let m = self.create_component_node(mount, idx, new, parent, dom, to);
|
// Remove the scope id from the mount
|
||||||
|
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = ScopeId::PLACEHOLDER.0;
|
||||||
|
let m = self.create_component_node(mount, idx, new, parent, dom, to.as_deref_mut());
|
||||||
|
|
||||||
// Instead of *just* removing it, we can use the replace mutation
|
// Instead of *just* removing it, we can use the replace mutation
|
||||||
dom.remove_component_node(to, scope, Some(m), true);
|
dom.remove_component_node(to, true, scope, Some(m));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create a new component (if it doesn't already exist) node and then mount the [`ScopeState`] for a component
|
||||||
|
///
|
||||||
|
/// Returns the number of nodes created on the stack
|
||||||
pub(super) fn create_component_node(
|
pub(super) fn create_component_node(
|
||||||
&self,
|
&self,
|
||||||
mount: MountId,
|
mount: MountId,
|
||||||
|
@ -115,19 +188,40 @@ impl VNode {
|
||||||
component: &VComponent,
|
component: &VComponent,
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
dom: &mut VirtualDom,
|
dom: &mut VirtualDom,
|
||||||
to: &mut impl WriteMutations,
|
to: Option<&mut impl WriteMutations>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
// Load up a ScopeId for this vcomponent. If it's already mounted, then we can just use that
|
// If this is a suspense boundary, run our suspense creation logic instead of running the component
|
||||||
let scope = dom
|
if component.props.props().type_id() == TypeId::of::<SuspenseBoundaryPropsWithOwner>() {
|
||||||
.new_scope(component.props.duplicate(), component.name)
|
return SuspenseBoundaryProps::create(mount, idx, component, parent, dom, to);
|
||||||
.state()
|
}
|
||||||
.id;
|
|
||||||
|
|
||||||
// Store the scope id for the next render
|
let mut scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]);
|
||||||
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope.0;
|
|
||||||
|
|
||||||
let new = dom.run_scope(scope);
|
// If the scopeid is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that
|
||||||
|
if scope_id.is_placeholder() {
|
||||||
|
scope_id = dom
|
||||||
|
.new_scope(component.props.duplicate(), component.name)
|
||||||
|
.state()
|
||||||
|
.id;
|
||||||
|
|
||||||
dom.create_scope(to, scope, new, parent)
|
// Store the scope id for the next render
|
||||||
|
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0;
|
||||||
|
|
||||||
|
// If this is a new scope, we also need to run it once to get the initial state
|
||||||
|
let new = dom.run_scope(scope_id);
|
||||||
|
|
||||||
|
// Then set the new node as the last rendered node
|
||||||
|
dom.scopes[scope_id.0].last_rendered_node = Some(new);
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]);
|
||||||
|
|
||||||
|
let new_node = dom.scopes[scope.0]
|
||||||
|
.last_rendered_node
|
||||||
|
.as_ref()
|
||||||
|
.expect("Component to be mounted")
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
dom.create_scope(to, scope, new_node, parent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
innerlude::{ElementRef, WriteMutations},
|
innerlude::{ElementRef, WriteMutations},
|
||||||
nodes::VNode,
|
nodes::VNode,
|
||||||
DynamicNode, ScopeId, TemplateNode, VirtualDom,
|
DynamicNode, ScopeId, VirtualDom,
|
||||||
};
|
};
|
||||||
|
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
@ -9,7 +9,7 @@ use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
pub(crate) fn diff_non_empty_fragment(
|
pub(crate) fn diff_non_empty_fragment(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
to: Option<&mut impl WriteMutations>,
|
||||||
old: &[VNode],
|
old: &[VNode],
|
||||||
new: &[VNode],
|
new: &[VNode],
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
|
@ -42,7 +42,7 @@ impl VirtualDom {
|
||||||
// the change list stack is in the same state when this function returns.
|
// the change list stack is in the same state when this function returns.
|
||||||
fn diff_non_keyed_children(
|
fn diff_non_keyed_children(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
old: &[VNode],
|
old: &[VNode],
|
||||||
new: &[VNode],
|
new: &[VNode],
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
|
@ -54,15 +54,18 @@ impl VirtualDom {
|
||||||
debug_assert!(!old.is_empty());
|
debug_assert!(!old.is_empty());
|
||||||
|
|
||||||
match old.len().cmp(&new.len()) {
|
match old.len().cmp(&new.len()) {
|
||||||
Ordering::Greater => self.remove_nodes(to, &old[new.len()..], None),
|
Ordering::Greater => self.remove_nodes(to.as_deref_mut(), &old[new.len()..], None),
|
||||||
Ordering::Less => {
|
Ordering::Less => self.create_and_insert_after(
|
||||||
self.create_and_insert_after(to, &new[old.len()..], old.last().unwrap(), parent)
|
to.as_deref_mut(),
|
||||||
}
|
&new[old.len()..],
|
||||||
|
old.last().unwrap(),
|
||||||
|
parent,
|
||||||
|
),
|
||||||
Ordering::Equal => {}
|
Ordering::Equal => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (new, old) in new.iter().zip(old.iter()) {
|
for (new, old) in new.iter().zip(old.iter()) {
|
||||||
old.diff_node(new, self, to);
|
old.diff_node(new, self, to.as_deref_mut());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +87,7 @@ impl VirtualDom {
|
||||||
// The stack is empty upon entry.
|
// The stack is empty upon entry.
|
||||||
fn diff_keyed_children(
|
fn diff_keyed_children(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
old: &[VNode],
|
old: &[VNode],
|
||||||
new: &[VNode],
|
new: &[VNode],
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
|
@ -116,10 +119,11 @@ impl VirtualDom {
|
||||||
//
|
//
|
||||||
// `shared_prefix_count` is the count of how many nodes at the start of
|
// `shared_prefix_count` is the count of how many nodes at the start of
|
||||||
// `new` and `old` share the same keys.
|
// `new` and `old` share the same keys.
|
||||||
let (left_offset, right_offset) = match self.diff_keyed_ends(to, old, new, parent) {
|
let (left_offset, right_offset) =
|
||||||
Some(count) => count,
|
match self.diff_keyed_ends(to.as_deref_mut(), old, new, parent) {
|
||||||
None => return,
|
Some(count) => count,
|
||||||
};
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
// Ok, we now hopefully have a smaller range of children in the middle
|
// Ok, we now hopefully have a smaller range of children in the middle
|
||||||
// within which to re-order nodes with the same keys, remove old nodes with
|
// within which to re-order nodes with the same keys, remove old nodes with
|
||||||
|
@ -164,7 +168,7 @@ impl VirtualDom {
|
||||||
/// If there is no offset, then this function returns None and the diffing is complete.
|
/// If there is no offset, then this function returns None and the diffing is complete.
|
||||||
fn diff_keyed_ends(
|
fn diff_keyed_ends(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
old: &[VNode],
|
old: &[VNode],
|
||||||
new: &[VNode],
|
new: &[VNode],
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
|
@ -176,7 +180,7 @@ impl VirtualDom {
|
||||||
if old.key != new.key {
|
if old.key != new.key {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
old.diff_node(new, self, to);
|
old.diff_node(new, self, to.as_deref_mut());
|
||||||
left_offset += 1;
|
left_offset += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,7 +205,7 @@ impl VirtualDom {
|
||||||
if old.key != new.key {
|
if old.key != new.key {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
old.diff_node(new, self, to);
|
old.diff_node(new, self, to.as_deref_mut());
|
||||||
right_offset += 1;
|
right_offset += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -224,7 +228,7 @@ impl VirtualDom {
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn diff_keyed_middle(
|
fn diff_keyed_middle(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
old: &[VNode],
|
old: &[VNode],
|
||||||
new: &[VNode],
|
new: &[VNode],
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
|
@ -236,7 +240,7 @@ impl VirtualDom {
|
||||||
- IE if we have ABCD becomes BACD, our sequence would be 1,0,2,3
|
- IE if we have ABCD becomes BACD, our sequence would be 1,0,2,3
|
||||||
- if we have ABCD to ABDE, our sequence would be 0,1,3,MAX because E doesn't exist
|
- if we have ABCD to ABDE, our sequence would be 0,1,3,MAX because E doesn't exist
|
||||||
|
|
||||||
now, we should have a list of integers that indicates where in the old list the new items mapto.
|
now, we should have a list of integers that indicates where in the old list the new items map to.
|
||||||
|
|
||||||
4. Compute the LIS of this list
|
4. Compute the LIS of this list
|
||||||
- this indicates the longest list of new children that won't need to be moved.
|
- this indicates the longest list of new children that won't need to be moved.
|
||||||
|
@ -257,11 +261,11 @@ impl VirtualDom {
|
||||||
|
|
||||||
// 1. Map the old keys into a numerical ordering based on indices.
|
// 1. Map the old keys into a numerical ordering based on indices.
|
||||||
// 2. Create a map of old key to its index
|
// 2. Create a map of old key to its index
|
||||||
// IE if the keys were A B C, then we would have (A, 1) (B, 2) (C, 3).
|
// IE if the keys were A B C, then we would have (A, 0) (B, 1) (C, 2).
|
||||||
let old_key_to_old_index = old
|
let old_key_to_old_index = old
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(i, o)| (o.key.as_ref().unwrap(), i))
|
.map(|(i, o)| (o.key.as_ref().unwrap().as_str(), i))
|
||||||
.collect::<FxHashMap<_, _>>();
|
.collect::<FxHashMap<_, _>>();
|
||||||
|
|
||||||
let mut shared_keys = FxHashSet::default();
|
let mut shared_keys = FxHashSet::default();
|
||||||
|
@ -271,163 +275,194 @@ impl VirtualDom {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|node| {
|
.map(|node| {
|
||||||
let key = node.key.as_ref().unwrap();
|
let key = node.key.as_ref().unwrap();
|
||||||
if let Some(&index) = old_key_to_old_index.get(&key) {
|
if let Some(&index) = old_key_to_old_index.get(key.as_str()) {
|
||||||
shared_keys.insert(key);
|
shared_keys.insert(key);
|
||||||
index
|
index
|
||||||
} else {
|
} else {
|
||||||
u32::MAX as usize
|
usize::MAX
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
// If none of the old keys are reused by the new children, then we remove all the remaining old children and
|
||||||
// create the new children afresh.
|
// create the new children afresh.
|
||||||
if shared_keys.is_empty() {
|
if shared_keys.is_empty() {
|
||||||
if !old.is_empty() {
|
debug_assert!(
|
||||||
let m = self.create_children(to, new, parent);
|
!old.is_empty(),
|
||||||
self.remove_nodes(to, old, Some(m));
|
"we should never be appending - just creating N"
|
||||||
} else {
|
);
|
||||||
// I think this is wrong - why are we appending?
|
|
||||||
// only valid of the if there are no trailing elements
|
let m = self.create_children(to.as_deref_mut(), new, parent);
|
||||||
// self.create_and_append_children(new);
|
self.remove_nodes(to, old, Some(m));
|
||||||
|
|
||||||
todo!("we should never be appending - just creating N");
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove any old children that are not shared
|
// remove any old children that are not shared
|
||||||
// todo: make this an iterator
|
for child_to_remove in old
|
||||||
for child in old {
|
.iter()
|
||||||
let key = child.key.as_ref().unwrap();
|
.filter(|child| !shared_keys.contains(child.key.as_ref().unwrap()))
|
||||||
if !shared_keys.contains(&key) {
|
{
|
||||||
child.remove_node(self, to, None, true);
|
child_to_remove.remove_node(self, to.as_deref_mut(), None);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Compute the LIS of this list
|
// 4. Compute the LIS of this list
|
||||||
let mut lis_sequence = Vec::with_capacity(new_index_to_old_index.len());
|
let mut lis_sequence = Vec::with_capacity(new_index_to_old_index.len());
|
||||||
|
|
||||||
let mut predecessors = vec![0; new_index_to_old_index.len()];
|
let mut allocation = vec![0; new_index_to_old_index.len() * 2];
|
||||||
let mut starts = vec![0; new_index_to_old_index.len()];
|
let (predecessors, starts) = allocation.split_at_mut(new_index_to_old_index.len());
|
||||||
|
|
||||||
longest_increasing_subsequence::lis_with(
|
longest_increasing_subsequence::lis_with(
|
||||||
&new_index_to_old_index,
|
&new_index_to_old_index,
|
||||||
&mut lis_sequence,
|
&mut lis_sequence,
|
||||||
|a, b| a < b,
|
|a, b| a < b,
|
||||||
&mut predecessors,
|
predecessors,
|
||||||
&mut starts,
|
starts,
|
||||||
);
|
);
|
||||||
|
|
||||||
// the lis comes out backwards, I think. can't quite tell.
|
|
||||||
lis_sequence.sort_unstable();
|
|
||||||
|
|
||||||
// if a new node gets u32 max and is at the end, then it might be part of our LIS (because u32 max is a valid LIS)
|
// if a new node gets u32 max and is at the end, then it might be part of our LIS (because u32 max is a valid LIS)
|
||||||
if lis_sequence.last().map(|f| new_index_to_old_index[*f]) == Some(u32::MAX as usize) {
|
if lis_sequence.first().map(|f| new_index_to_old_index[*f]) == Some(usize::MAX) {
|
||||||
lis_sequence.pop();
|
lis_sequence.remove(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Diff each nod in the LIS
|
||||||
for idx in &lis_sequence {
|
for idx in &lis_sequence {
|
||||||
old[new_index_to_old_index[*idx]].diff_node(&new[*idx], self, to);
|
old[new_index_to_old_index[*idx]].diff_node(&new[*idx], self, to.as_deref_mut());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut nodes_created = 0;
|
/// Create or diff each node in a range depending on whether it is in the LIS or not
|
||||||
|
/// Returns the number of nodes created on the stack
|
||||||
|
fn create_or_diff(
|
||||||
|
vdom: &mut VirtualDom,
|
||||||
|
new: &[VNode],
|
||||||
|
old: &[VNode],
|
||||||
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
|
parent: Option<ElementRef>,
|
||||||
|
new_index_to_old_index: &[usize],
|
||||||
|
range: std::ops::Range<usize>,
|
||||||
|
) -> usize {
|
||||||
|
let range_start = range.start;
|
||||||
|
new[range]
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(idx, new_node)| {
|
||||||
|
let new_idx = range_start + idx;
|
||||||
|
let old_index = new_index_to_old_index[new_idx];
|
||||||
|
// If the node existed in the old list, diff it
|
||||||
|
if let Some(old_node) = old.get(old_index) {
|
||||||
|
old_node.diff_node(new_node, vdom, to.as_deref_mut());
|
||||||
|
if let Some(to) = to.as_deref_mut() {
|
||||||
|
new_node.push_all_root_nodes(vdom, to)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, just add it to the stack
|
||||||
|
new_node.create(vdom, parent, to.as_deref_mut())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum()
|
||||||
|
}
|
||||||
|
|
||||||
// add mount instruction for the first items not covered by the lis
|
// add mount instruction for the items before the LIS
|
||||||
let last = *lis_sequence.last().unwrap();
|
let last = *lis_sequence.first().unwrap();
|
||||||
if last < (new.len() - 1) {
|
if last < (new.len() - 1) {
|
||||||
for (idx, new_node) in new[(last + 1)..].iter().enumerate() {
|
let nodes_created = create_or_diff(
|
||||||
let new_idx = idx + last + 1;
|
self,
|
||||||
let old_index = new_index_to_old_index[new_idx];
|
new,
|
||||||
if old_index == u32::MAX as usize {
|
old,
|
||||||
nodes_created += new_node.create(self, to, parent);
|
to.as_deref_mut(),
|
||||||
} else {
|
parent,
|
||||||
old[old_index].diff_node(new_node, self, to);
|
&new_index_to_old_index,
|
||||||
nodes_created += new_node.push_all_real_nodes(self, to);
|
(last + 1)..new.len(),
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
let id = new[last].find_last_element(self);
|
// Insert all the nodes that we just created after the last node in the LIS
|
||||||
if nodes_created > 0 {
|
self.insert_after(to.as_deref_mut(), nodes_created, &new[last]);
|
||||||
to.insert_nodes_after(id, nodes_created)
|
|
||||||
}
|
|
||||||
nodes_created = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// for each spacing, generate a mount instruction
|
// For each node inside of the LIS, but not included in the LIS, generate a mount instruction
|
||||||
let mut lis_iter = lis_sequence.iter().rev();
|
// We loop over the LIS in reverse order and insert any nodes we find in the gaps between indexes
|
||||||
|
let mut lis_iter = lis_sequence.iter();
|
||||||
let mut last = *lis_iter.next().unwrap();
|
let mut last = *lis_iter.next().unwrap();
|
||||||
for next in lis_iter {
|
for next in lis_iter {
|
||||||
if last - next > 1 {
|
if last - next > 1 {
|
||||||
for (idx, new_node) in new[(next + 1)..last].iter().enumerate() {
|
let nodes_created = create_or_diff(
|
||||||
let new_idx = idx + next + 1;
|
self,
|
||||||
let old_index = new_index_to_old_index[new_idx];
|
new,
|
||||||
if old_index == u32::MAX as usize {
|
old,
|
||||||
nodes_created += new_node.create(self, to, parent);
|
to.as_deref_mut(),
|
||||||
} else {
|
parent,
|
||||||
old[old_index].diff_node(new_node, self, to);
|
&new_index_to_old_index,
|
||||||
nodes_created += new_node.push_all_real_nodes(self, to);
|
(next + 1)..last,
|
||||||
}
|
);
|
||||||
}
|
|
||||||
|
|
||||||
let id = new[last].find_first_element(self);
|
self.insert_before(to.as_deref_mut(), nodes_created, &new[last]);
|
||||||
if nodes_created > 0 {
|
|
||||||
to.insert_nodes_before(id, nodes_created);
|
|
||||||
}
|
|
||||||
|
|
||||||
nodes_created = 0;
|
|
||||||
}
|
}
|
||||||
last = *next;
|
last = *next;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add mount instruction for the last items not covered by the lis
|
// add mount instruction for the items after the LIS
|
||||||
let first_lis = *lis_sequence.first().unwrap();
|
let first_lis = *lis_sequence.last().unwrap();
|
||||||
if first_lis > 0 {
|
if first_lis > 0 {
|
||||||
for (idx, new_node) in new[..first_lis].iter().enumerate() {
|
let nodes_created = create_or_diff(
|
||||||
let old_index = new_index_to_old_index[idx];
|
self,
|
||||||
if old_index == u32::MAX as usize {
|
new,
|
||||||
nodes_created += new_node.create(self, to, parent);
|
old,
|
||||||
} else {
|
to.as_deref_mut(),
|
||||||
old[old_index].diff_node(new_node, self, to);
|
parent,
|
||||||
nodes_created += new_node.push_all_real_nodes(self, to);
|
&new_index_to_old_index,
|
||||||
}
|
0..first_lis,
|
||||||
}
|
);
|
||||||
|
|
||||||
let id = new[first_lis].find_first_element(self);
|
self.insert_before(to, nodes_created, &new[first_lis]);
|
||||||
if nodes_created > 0 {
|
|
||||||
to.insert_nodes_before(id, nodes_created);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_and_insert_before(
|
fn create_and_insert_before(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
new: &[VNode],
|
new: &[VNode],
|
||||||
before: &VNode,
|
before: &VNode,
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
) {
|
) {
|
||||||
let m = self.create_children(to, new, parent);
|
let m = self.create_children(to.as_deref_mut(), new, parent);
|
||||||
let id = before.find_first_element(self);
|
self.insert_before(to, m, before);
|
||||||
to.insert_nodes_before(id, m);
|
}
|
||||||
|
|
||||||
|
fn insert_before(&mut self, to: Option<&mut impl WriteMutations>, new: usize, before: &VNode) {
|
||||||
|
if let Some(to) = to {
|
||||||
|
if new > 0 {
|
||||||
|
let id = before.find_first_element(self);
|
||||||
|
to.insert_nodes_before(id, new);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn create_and_insert_after(
|
fn create_and_insert_after(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
new: &[VNode],
|
new: &[VNode],
|
||||||
after: &VNode,
|
after: &VNode,
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
) {
|
) {
|
||||||
let m = self.create_children(to, new, parent);
|
let m = self.create_children(to.as_deref_mut(), new, parent);
|
||||||
let id = after.find_last_element(self);
|
self.insert_after(to, m, after);
|
||||||
to.insert_nodes_after(id, m);
|
}
|
||||||
|
|
||||||
|
fn insert_after(&mut self, to: Option<&mut impl WriteMutations>, new: usize, after: &VNode) {
|
||||||
|
if let Some(to) = to {
|
||||||
|
if new > 0 {
|
||||||
|
let id = after.find_last_element(self);
|
||||||
|
to.insert_nodes_after(id, new);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VNode {
|
impl VNode {
|
||||||
/// Push all the real nodes on the stack
|
/// Push all the root nodes on the stack
|
||||||
pub(crate) fn push_all_real_nodes(
|
pub(crate) fn push_all_root_nodes(
|
||||||
&self,
|
&self,
|
||||||
dom: &VirtualDom,
|
dom: &VirtualDom,
|
||||||
to: &mut impl WriteMutations,
|
to: &mut impl WriteMutations,
|
||||||
|
@ -440,30 +475,27 @@ impl VNode {
|
||||||
.roots
|
.roots
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(root_idx, _)| match &self.template.get().roots[root_idx] {
|
.map(
|
||||||
TemplateNode::Dynamic { id: idx } => match &self.dynamic_nodes[*idx] {
|
|(root_idx, _)| match self.get_dynamic_root_node_and_id(root_idx) {
|
||||||
DynamicNode::Placeholder(_) | DynamicNode::Text(_) => {
|
Some((_, DynamicNode::Fragment(nodes))) => {
|
||||||
to.push_root(mount.root_ids[root_idx]);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
DynamicNode::Fragment(nodes) => {
|
|
||||||
let mut accumulated = 0;
|
let mut accumulated = 0;
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
accumulated += node.push_all_real_nodes(dom, to);
|
accumulated += node.push_all_root_nodes(dom, to);
|
||||||
}
|
}
|
||||||
accumulated
|
accumulated
|
||||||
}
|
}
|
||||||
DynamicNode::Component(_) => {
|
Some((idx, DynamicNode::Component(_))) => {
|
||||||
let scope = ScopeId(mount.mounted_dynamic_nodes[*idx]);
|
let scope = ScopeId(mount.mounted_dynamic_nodes[idx]);
|
||||||
let node = dom.get_scope(scope).unwrap().root_node();
|
let node = dom.get_scope(scope).unwrap().root_node();
|
||||||
node.push_all_real_nodes(dom, to)
|
node.push_all_root_nodes(dom, to)
|
||||||
|
}
|
||||||
|
// This is a static root node or a single dynamic node, just push it
|
||||||
|
None | Some((_, DynamicNode::Placeholder(_) | DynamicNode::Text(_))) => {
|
||||||
|
to.push_root(mount.root_ids[root_idx]);
|
||||||
|
1
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => {
|
)
|
||||||
to.push_root(mount.root_ids[root_idx]);
|
|
||||||
1
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
|
//! This module contains all the code for creating and diffing nodes.
|
||||||
|
//!
|
||||||
|
//! For suspense there are three different cases we need to handle:
|
||||||
|
//! - Creating nodes/scopes without mounting them
|
||||||
|
//! - Diffing nodes that are not mounted
|
||||||
|
//! - Mounted nodes that have already been created
|
||||||
|
//!
|
||||||
|
//! To support those cases, we lazily create components and only optionally write to the real dom while diffing with Option<&mut impl WriteMutations>
|
||||||
|
|
||||||
#![allow(clippy::too_many_arguments)]
|
#![allow(clippy::too_many_arguments)]
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
arena::ElementId,
|
arena::ElementId,
|
||||||
innerlude::{ElementRef, MountId, WriteMutations},
|
innerlude::{ElementRef, MountId, WriteMutations},
|
||||||
nodes::VNode,
|
nodes::VNode,
|
||||||
scopes::ScopeId,
|
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
Template, TemplateNode,
|
Template, TemplateNode,
|
||||||
};
|
};
|
||||||
|
@ -14,34 +22,36 @@ mod iterator;
|
||||||
mod node;
|
mod node;
|
||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
pub(crate) fn create_children<'a>(
|
pub(crate) fn create_children(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
nodes: impl IntoIterator<Item = &'a VNode>,
|
nodes: &[VNode],
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
) -> usize {
|
) -> usize {
|
||||||
nodes
|
nodes
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|child| child.create(self, to, parent))
|
.map(|child| child.create(self, parent, to.as_deref_mut()))
|
||||||
.sum()
|
.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simply replace a placeholder with a list of nodes
|
/// Simply replace a placeholder with a list of nodes
|
||||||
fn replace_placeholder<'a>(
|
fn replace_placeholder(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
placeholder_id: ElementId,
|
placeholder_id: ElementId,
|
||||||
r: impl IntoIterator<Item = &'a VNode>,
|
r: &[VNode],
|
||||||
parent: Option<ElementRef>,
|
parent: Option<ElementRef>,
|
||||||
) {
|
) {
|
||||||
let m = self.create_children(to, r, parent);
|
let m = self.create_children(to.as_deref_mut(), r, parent);
|
||||||
to.replace_node_with(placeholder_id, m);
|
if let Some(to) = to {
|
||||||
self.reclaim(placeholder_id);
|
to.replace_node_with(placeholder_id, m);
|
||||||
|
self.reclaim(placeholder_id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nodes_to_placeholder(
|
fn nodes_to_placeholder(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
mount: MountId,
|
mount: MountId,
|
||||||
dyn_node_idx: usize,
|
dyn_node_idx: usize,
|
||||||
old_nodes: &[VNode],
|
old_nodes: &[VNode],
|
||||||
|
@ -52,13 +62,15 @@ impl VirtualDom {
|
||||||
// Set the id of the placeholder
|
// Set the id of the placeholder
|
||||||
self.mounts[mount.0].mounted_dynamic_nodes[dyn_node_idx] = placeholder.0;
|
self.mounts[mount.0].mounted_dynamic_nodes[dyn_node_idx] = placeholder.0;
|
||||||
|
|
||||||
to.create_placeholder(placeholder);
|
if let Some(to) = to.as_deref_mut() {
|
||||||
|
to.create_placeholder(placeholder);
|
||||||
|
}
|
||||||
|
|
||||||
self.replace_nodes(to, old_nodes, 1);
|
self.replace_nodes(to, old_nodes, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace many nodes with a number of nodes on the stack
|
/// Replace many nodes with a number of nodes on the stack
|
||||||
fn replace_nodes(&mut self, to: &mut impl WriteMutations, nodes: &[VNode], m: usize) {
|
fn replace_nodes(&mut self, to: Option<&mut impl WriteMutations>, nodes: &[VNode], m: usize) {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
!nodes.is_empty(),
|
!nodes.is_empty(),
|
||||||
"replace_nodes must have at least one node"
|
"replace_nodes must have at least one node"
|
||||||
|
@ -73,32 +85,16 @@ impl VirtualDom {
|
||||||
/// Wont generate mutations for the inner nodes
|
/// Wont generate mutations for the inner nodes
|
||||||
fn remove_nodes(
|
fn remove_nodes(
|
||||||
&mut self,
|
&mut self,
|
||||||
to: &mut impl WriteMutations,
|
mut to: Option<&mut impl WriteMutations>,
|
||||||
nodes: &[VNode],
|
nodes: &[VNode],
|
||||||
replace_with: Option<usize>,
|
replace_with: Option<usize>,
|
||||||
) {
|
) {
|
||||||
for (i, node) in nodes.iter().rev().enumerate() {
|
for (i, node) in nodes.iter().rev().enumerate() {
|
||||||
let last_node = i == nodes.len() - 1;
|
let last_node = i == nodes.len() - 1;
|
||||||
node.remove_node(self, to, replace_with.filter(|_| last_node), true);
|
node.remove_node(self, to.as_deref_mut(), replace_with.filter(|_| last_node));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_component_node(
|
|
||||||
&mut self,
|
|
||||||
to: &mut impl WriteMutations,
|
|
||||||
scope: ScopeId,
|
|
||||||
replace_with: Option<usize>,
|
|
||||||
gen_muts: bool,
|
|
||||||
) {
|
|
||||||
// Remove the component from the dom
|
|
||||||
if let Some(node) = self.scopes[scope.0].last_rendered_node.take() {
|
|
||||||
node.remove_node(self, to, replace_with, gen_muts)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Now drop all the resources
|
|
||||||
self.drop_scope(scope);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Insert a new template into the VirtualDom's template registry
|
/// Insert a new template into the VirtualDom's template registry
|
||||||
// used in conditional compilation
|
// used in conditional compilation
|
||||||
#[allow(unused_mut)]
|
#[allow(unused_mut)]
|
||||||
|
@ -107,40 +103,37 @@ impl VirtualDom {
|
||||||
to: &mut impl WriteMutations,
|
to: &mut impl WriteMutations,
|
||||||
mut template: Template,
|
mut template: Template,
|
||||||
) {
|
) {
|
||||||
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
// In debug mode, we check the more complete hashmap by byte index
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
let byte_index = byte_index.parse::<usize>().unwrap();
|
|
||||||
// First, check if we've already seen this template
|
|
||||||
if self
|
|
||||||
.templates
|
|
||||||
.get(&path)
|
|
||||||
.filter(|set| set.contains_key(&byte_index))
|
|
||||||
.is_none()
|
|
||||||
{
|
{
|
||||||
// if hot reloading is enabled, then we need to check for a template that has overriten this one
|
let (path, byte_index) = template.name.rsplit_once(':').unwrap();
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
if let Some(mut new_template) = self
|
let byte_index = byte_index.parse::<usize>().unwrap();
|
||||||
.templates
|
let mut entry = self.templates.entry(path);
|
||||||
.get_mut(path)
|
// If we've already seen this template, just return
|
||||||
.and_then(|map| map.remove(&usize::MAX))
|
if let std::collections::hash_map::Entry::Occupied(occupied) = &entry {
|
||||||
{
|
if occupied.get().contains_key(&byte_index) {
|
||||||
// the byte index of the hot reloaded template could be different
|
return;
|
||||||
new_template.name = template.name;
|
}
|
||||||
template = new_template;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.templates
|
// Otherwise, insert it and register it
|
||||||
.entry(path)
|
entry.or_default().insert(byte_index, template);
|
||||||
.or_default()
|
}
|
||||||
.insert(byte_index, template);
|
|
||||||
|
|
||||||
// If it's all dynamic nodes, then we don't need to register it
|
// In release mode, everything is built into the &'static str
|
||||||
if !template.is_completely_dynamic() {
|
#[cfg(not(debug_assertions))]
|
||||||
to.register_template(template)
|
if !self.templates.insert(template.name) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's all dynamic nodes, then we don't need to register it
|
||||||
|
if !template.is_completely_dynamic() {
|
||||||
|
to.register_template(template)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
/// Insert a new template into the VirtualDom's template registry
|
/// Insert a new template into the VirtualDom's template registry
|
||||||
pub(crate) fn register_template_first_byte_index(&mut self, mut template: Template) {
|
pub(crate) fn register_template_first_byte_index(&mut self, mut template: Template) {
|
||||||
// First, make sure we mark the template as seen, regardless if we process it
|
// First, make sure we mark the template as seen, regardless if we process it
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -16,9 +16,9 @@ pub(crate) struct Effect {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Effect {
|
impl Effect {
|
||||||
pub(crate) fn new(order: ScopeOrder, f: impl FnOnce() + 'static) -> Self {
|
pub(crate) fn new(order: ScopeOrder, f: Box<dyn FnOnce() + 'static>) -> Self {
|
||||||
let mut effect = VecDeque::new();
|
let mut effect = VecDeque::new();
|
||||||
effect.push_back(Box::new(f) as Box<dyn FnOnce() + 'static>);
|
effect.push_back(f);
|
||||||
Self {
|
Self {
|
||||||
order,
|
order,
|
||||||
effect: RefCell::new(effect),
|
effect: RefCell::new(effect),
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
global_context::{current_scope_id, try_consume_context},
|
global_context::current_scope_id, innerlude::provide_context, use_hook, Element, IntoDynNode,
|
||||||
innerlude::provide_context,
|
Properties, ScopeId, Template, TemplateAttribute, TemplateNode, VNode,
|
||||||
use_hook, Element, IntoDynNode, Properties, ScopeId, Template, TemplateAttribute, TemplateNode,
|
|
||||||
VNode,
|
|
||||||
};
|
};
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
backtrace::Backtrace,
|
backtrace::Backtrace,
|
||||||
cell::RefCell,
|
cell::{Ref, RefCell},
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt::{Debug, Display},
|
fmt::{Debug, Display},
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A panic in a component that was caught by an error boundary.
|
/// A panic in a component that was caught by an error boundary.
|
||||||
///
|
///
|
||||||
/// NOTE: WASM currently does not support caching unwinds, so this struct will not be created in WASM.
|
/// <div class="warning">
|
||||||
|
///
|
||||||
|
/// WASM currently does not support caching unwinds, so this struct will not be created in WASM.
|
||||||
|
///
|
||||||
|
/// </div>
|
||||||
pub struct CapturedPanic {
|
pub struct CapturedPanic {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
/// The error that was caught
|
/// The error that was caught
|
||||||
|
@ -28,66 +31,465 @@ impl Debug for CapturedPanic {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provide an error boundary to catch errors from child components
|
impl Display for CapturedPanic {
|
||||||
pub fn use_error_boundary() -> ErrorBoundary {
|
|
||||||
use_hook(|| provide_context(ErrorBoundary::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A boundary that will capture any errors from child components
|
|
||||||
#[derive(Debug, Clone, Default)]
|
|
||||||
pub struct ErrorBoundary {
|
|
||||||
inner: Rc<ErrorBoundaryInner>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A boundary that will capture any errors from child components
|
|
||||||
pub struct ErrorBoundaryInner {
|
|
||||||
error: RefCell<Option<CapturedError>>,
|
|
||||||
_id: ScopeId,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Debug for ErrorBoundaryInner {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.debug_struct("ErrorBoundaryInner")
|
f.write_fmt(format_args!("Encountered panic: {:?}", self.error))
|
||||||
.field("error", &self.error)
|
|
||||||
.finish()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for any type that can be downcast to a concrete type and implements Debug. This is automatically implemented for all types that implement Any + Debug.
|
impl Error for CapturedPanic {}
|
||||||
pub trait AnyDebug: Any + Debug {
|
|
||||||
fn as_any(&self) -> &dyn Any;
|
/// Provide an error boundary to catch errors from child components
|
||||||
|
pub fn use_error_boundary() -> ErrorContext {
|
||||||
|
use_hook(|| provide_context(ErrorContext::new(Vec::new(), current_scope_id().unwrap())))
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Any + Debug> AnyDebug for T {
|
/// A trait for any type that can be downcast to a concrete type and implements Debug. This is automatically implemented for all types that implement Any + Debug.
|
||||||
|
pub trait AnyError {
|
||||||
|
fn as_any(&self) -> &dyn Any;
|
||||||
|
fn as_error(&self) -> &dyn Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An wrapper error type for types that only implement Display. We use a inner type here to avoid overlapping implementations for DisplayError and impl Error
|
||||||
|
struct DisplayError(DisplayErrorInner);
|
||||||
|
|
||||||
|
impl<E: Display + 'static> From<E> for DisplayError {
|
||||||
|
fn from(e: E) -> Self {
|
||||||
|
Self(DisplayErrorInner(Box::new(e)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DisplayErrorInner(Box<dyn Display>);
|
||||||
|
impl Display for DisplayErrorInner {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for DisplayErrorInner {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for DisplayErrorInner {}
|
||||||
|
|
||||||
|
impl AnyError for DisplayError {
|
||||||
fn as_any(&self) -> &dyn Any {
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
&self.0 .0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_error(&self) -> &dyn Error {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides context methods to [`Result`] and [`Option`] types that are compatible with [`CapturedError`]
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented outside of dioxus-core
|
||||||
|
pub trait Context<T, E>: private::Sealed {
|
||||||
|
/// Add a visual representation of the error that the [`ErrorBoundary`] may render
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn Component() -> Element {
|
||||||
|
/// // You can bubble up errors with `?` inside components, and event handlers
|
||||||
|
/// // Along with the error itself, you can provide a way to display the error by calling `show`
|
||||||
|
/// let number = "1234".parse::<usize>().show(|error| rsx! {
|
||||||
|
/// div {
|
||||||
|
/// background_color: "red",
|
||||||
|
/// color: "white",
|
||||||
|
/// "Error parsing number: {error}"
|
||||||
|
/// }
|
||||||
|
/// })?;
|
||||||
|
/// todo!()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn show(self, display_error: impl FnOnce(&E) -> Element) -> Result<T>;
|
||||||
|
|
||||||
|
/// Wrap the result additional context about the error that occurred.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn NumberParser() -> Element {
|
||||||
|
/// // You can bubble up errors with `?` inside components, and event handlers
|
||||||
|
/// // Along with the error itself, you can provide a way to display the error by calling `context`
|
||||||
|
/// let number = "-1234".parse::<usize>().context("Parsing number inside of the NumberParser")?;
|
||||||
|
/// todo!()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn context<C: Display + 'static>(self, context: C) -> Result<T>;
|
||||||
|
|
||||||
|
/// Wrap the result with additional context about the error that occurred. The closure will only be run if the Result is an error.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn NumberParser() -> Element {
|
||||||
|
/// // You can bubble up errors with `?` inside components, and event handlers
|
||||||
|
/// // Along with the error itself, you can provide a way to display the error by calling `context`
|
||||||
|
/// let number = "-1234".parse::<usize>().with_context(|| format!("Timestamp: {:?}", std::time::Instant::now()))?;
|
||||||
|
/// todo!()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
fn with_context<C: Display + 'static>(self, context: impl FnOnce() -> C) -> Result<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T, E> Context<T, E> for std::result::Result<T, E>
|
||||||
|
where
|
||||||
|
E: Error + 'static,
|
||||||
|
{
|
||||||
|
fn show(self, display_error: impl FnOnce(&E) -> Element) -> Result<T> {
|
||||||
|
// We don't use result mapping to avoid extra frames
|
||||||
|
match self {
|
||||||
|
std::result::Result::Ok(value) => Ok(value),
|
||||||
|
Err(error) => {
|
||||||
|
let render = display_error(&error).unwrap_or_default();
|
||||||
|
let mut error: CapturedError = error.into();
|
||||||
|
error.render = render;
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context<C: Display + 'static>(self, context: C) -> Result<T> {
|
||||||
|
self.with_context(|| context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_context<C: Display + 'static>(self, context: impl FnOnce() -> C) -> Result<T> {
|
||||||
|
// We don't use result mapping to avoid extra frames
|
||||||
|
match self {
|
||||||
|
std::result::Result::Ok(value) => Ok(value),
|
||||||
|
Err(error) => {
|
||||||
|
let mut error: CapturedError = error.into();
|
||||||
|
error.context.push(Rc::new(AdditionalErrorContext {
|
||||||
|
backtrace: Backtrace::capture(),
|
||||||
|
context: Box::new(context()),
|
||||||
|
scope: current_scope_id(),
|
||||||
|
}));
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Context<T, CapturedError> for Option<T> {
|
||||||
|
fn show(self, display_error: impl FnOnce(&CapturedError) -> Element) -> Result<T> {
|
||||||
|
// We don't use result mapping to avoid extra frames
|
||||||
|
match self {
|
||||||
|
Some(value) => Ok(value),
|
||||||
|
None => {
|
||||||
|
let mut error = CapturedError::from_display("Value was none");
|
||||||
|
let render = display_error(&error).unwrap_or_default();
|
||||||
|
error.render = render;
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn context<C: Display + 'static>(self, context: C) -> Result<T> {
|
||||||
|
self.with_context(|| context)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_context<C: Display + 'static>(self, context: impl FnOnce() -> C) -> Result<T> {
|
||||||
|
// We don't use result mapping to avoid extra frames
|
||||||
|
match self {
|
||||||
|
Some(value) => Ok(value),
|
||||||
|
None => {
|
||||||
|
let error = CapturedError::from_display(context());
|
||||||
|
Err(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod private {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub trait Sealed {}
|
||||||
|
|
||||||
|
impl<T, E> Sealed for std::result::Result<T, E> where E: Error {}
|
||||||
|
impl<T> Sealed for Option<T> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Any + Error> AnyError for T {
|
||||||
|
fn as_any(&self) -> &dyn Any {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_error(&self) -> &dyn Error {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
/// A context with information about suspended components
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ErrorContext {
|
||||||
|
errors: Rc<RefCell<Vec<CapturedError>>>,
|
||||||
|
id: ScopeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for ErrorContext {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.errors, &other.errors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ErrorContext {
|
||||||
|
/// Create a new suspense boundary in a specific scope
|
||||||
|
pub(crate) fn new(errors: Vec<CapturedError>, id: ScopeId) -> Self {
|
||||||
|
Self {
|
||||||
|
errors: Rc::new(RefCell::new(errors)),
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all errors thrown from child components
|
||||||
|
pub fn errors(&self) -> Ref<[CapturedError]> {
|
||||||
|
Ref::map(self.errors.borrow(), |errors| errors.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the Element from the first error that can be shown
|
||||||
|
pub fn show(&self) -> Option<Element> {
|
||||||
|
self.errors.borrow().iter().find_map(|task| task.show())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push an error into this Error Boundary
|
||||||
|
pub fn insert_error(&self, error: CapturedError) {
|
||||||
|
self.errors.borrow_mut().push(error);
|
||||||
|
self.id.needs_update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all errors from this Error Boundary
|
||||||
|
pub fn clear_errors(&self) {
|
||||||
|
self.errors.borrow_mut().clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Errors can have additional context added as they bubble up the render tree
|
||||||
|
/// This context can be used to provide additional information to the user
|
||||||
|
struct AdditionalErrorContext {
|
||||||
|
backtrace: Backtrace,
|
||||||
|
context: Box<dyn Display>,
|
||||||
|
scope: Option<ScopeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for AdditionalErrorContext {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("ErrorContext")
|
||||||
|
.field("backtrace", &self.backtrace)
|
||||||
|
.field("context", &self.context.to_string())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AdditionalErrorContext {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let AdditionalErrorContext {
|
||||||
|
backtrace,
|
||||||
|
context,
|
||||||
|
scope,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
write!(f, "{context} (from ")?;
|
||||||
|
|
||||||
|
if let Some(scope) = scope {
|
||||||
|
write!(f, "scope {scope:?} ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
write!(f, "at {backtrace:?})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A type alias for a result that can be either a boxed error or a value
|
||||||
|
/// This is useful to avoid having to use `Result<T, CapturedError>` everywhere
|
||||||
|
pub type Result<T = ()> = std::result::Result<T, CapturedError>;
|
||||||
|
|
||||||
|
/// A helper function for an Ok result that can be either a boxed error or a value
|
||||||
|
/// This is useful to avoid having to use `Ok<T, CapturedError>` everywhere
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn Ok<T>(value: T) -> Result<T> {
|
||||||
|
Result::Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
/// An instance of an error captured by a descendant component.
|
/// An instance of an error captured by a descendant component.
|
||||||
pub struct CapturedError {
|
pub struct CapturedError {
|
||||||
/// The error captured by the error boundary
|
/// The error captured by the error boundary
|
||||||
pub error: Box<dyn AnyDebug + 'static>,
|
error: Rc<dyn AnyError + 'static>,
|
||||||
|
|
||||||
/// The backtrace of the error
|
/// The backtrace of the error
|
||||||
pub backtrace: Backtrace,
|
backtrace: Rc<Backtrace>,
|
||||||
|
|
||||||
/// The scope that threw the error
|
/// The scope that threw the error
|
||||||
pub scope: ScopeId,
|
scope: ScopeId,
|
||||||
|
|
||||||
|
/// An error message that can be displayed to the user
|
||||||
|
pub(crate) render: VNode,
|
||||||
|
|
||||||
|
/// Additional context that was added to the error
|
||||||
|
context: Vec<Rc<AdditionalErrorContext>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for CapturedError {
|
||||||
|
type Err = std::convert::Infallible;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
|
std::result::Result::Ok(Self::from_display(s.to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
struct SerializedCapturedError {
|
||||||
|
error: String,
|
||||||
|
context: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
impl serde::Serialize for CapturedError {
|
||||||
|
fn serialize<S: serde::Serializer>(
|
||||||
|
&self,
|
||||||
|
serializer: S,
|
||||||
|
) -> std::result::Result<S::Ok, S::Error> {
|
||||||
|
let serialized = SerializedCapturedError {
|
||||||
|
error: self.error.as_error().to_string(),
|
||||||
|
context: self
|
||||||
|
.context
|
||||||
|
.iter()
|
||||||
|
.map(|context| context.to_string())
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
serialized.serialize(serializer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "serialize")]
|
||||||
|
impl<'de> serde::Deserialize<'de> for CapturedError {
|
||||||
|
fn deserialize<D: serde::Deserializer<'de>>(
|
||||||
|
deserializer: D,
|
||||||
|
) -> std::result::Result<Self, D::Error> {
|
||||||
|
let serialized = SerializedCapturedError::deserialize(deserializer)?;
|
||||||
|
|
||||||
|
let error = DisplayError::from(serialized.error);
|
||||||
|
let context = serialized
|
||||||
|
.context
|
||||||
|
.into_iter()
|
||||||
|
.map(|context| {
|
||||||
|
Rc::new(AdditionalErrorContext {
|
||||||
|
scope: None,
|
||||||
|
backtrace: Backtrace::disabled(),
|
||||||
|
context: Box::new(context),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
std::result::Result::Ok(Self {
|
||||||
|
error: Rc::new(error),
|
||||||
|
context,
|
||||||
|
backtrace: Rc::new(Backtrace::disabled()),
|
||||||
|
scope: ScopeId::ROOT,
|
||||||
|
render: VNode::placeholder(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for CapturedError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("CapturedError")
|
||||||
|
.field("error", &self.error.as_error())
|
||||||
|
.field("backtrace", &self.backtrace)
|
||||||
|
.field("scope", &self.scope)
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: AnyError + 'static> From<E> for CapturedError {
|
||||||
|
fn from(error: E) -> Self {
|
||||||
|
Self {
|
||||||
|
error: Rc::new(error),
|
||||||
|
backtrace: Rc::new(Backtrace::capture()),
|
||||||
|
scope: current_scope_id()
|
||||||
|
.expect("Cannot create an error boundary outside of a component's scope."),
|
||||||
|
render: Default::default(),
|
||||||
|
context: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CapturedError {
|
||||||
|
/// Create a new captured error
|
||||||
|
pub fn new(error: impl AnyError + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
error: Rc::new(error),
|
||||||
|
backtrace: Rc::new(Backtrace::capture()),
|
||||||
|
scope: current_scope_id().unwrap_or(ScopeId::ROOT),
|
||||||
|
render: Default::default(),
|
||||||
|
context: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new error from a type that only implements [`Display`]. If your type implements [`Error`], you can use [`CapturedError::from`] instead.
|
||||||
|
pub fn from_display(error: impl Display + 'static) -> Self {
|
||||||
|
Self {
|
||||||
|
error: Rc::new(DisplayError::from(error)),
|
||||||
|
backtrace: Rc::new(Backtrace::capture()),
|
||||||
|
scope: current_scope_id().unwrap_or(ScopeId::ROOT),
|
||||||
|
render: Default::default(),
|
||||||
|
context: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mark the error as being thrown from a specific scope
|
||||||
|
pub fn with_origin(mut self, scope: ScopeId) -> Self {
|
||||||
|
self.scope = scope;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clone the error while retaining the mounted information of the error
|
||||||
|
pub(crate) fn clone_mounted(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
error: self.error.clone(),
|
||||||
|
backtrace: self.backtrace.clone(),
|
||||||
|
scope: self.scope,
|
||||||
|
render: self.render.clone_mounted(),
|
||||||
|
context: self.context.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a VNode representation of the error if the error provides one
|
||||||
|
pub fn show(&self) -> Option<Element> {
|
||||||
|
if self.render == VNode::placeholder() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(std::result::Result::Ok(self.render.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for CapturedError {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
format!("{:?}", self) == format!("{:?}", other)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for CapturedError {
|
impl Display for CapturedError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_fmt(format_args!(
|
f.write_fmt(format_args!(
|
||||||
"Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}",
|
"Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}\nContext: ",
|
||||||
self.error, self.scope, self.backtrace
|
self.error.as_error(),
|
||||||
))
|
self.scope,
|
||||||
|
self.backtrace
|
||||||
|
))?;
|
||||||
|
for context in &*self.context {
|
||||||
|
f.write_fmt(format_args!("{}\n", context))?;
|
||||||
|
}
|
||||||
|
std::result::Result::Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Error for CapturedError {}
|
|
||||||
|
|
||||||
impl CapturedError {
|
impl CapturedError {
|
||||||
/// Downcast the error type into a concrete error type
|
/// Downcast the error type into a concrete error type
|
||||||
pub fn downcast<T: 'static>(&self) -> Option<&T> {
|
pub fn downcast<T: 'static>(&self) -> Option<&T> {
|
||||||
|
@ -99,217 +501,69 @@ impl CapturedError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ErrorBoundaryInner {
|
pub(crate) fn throw_into(error: impl Into<CapturedError>, scope: ScopeId) {
|
||||||
fn default() -> Self {
|
let error = error.into();
|
||||||
Self {
|
if let Some(cx) = scope.consume_context::<ErrorContext>() {
|
||||||
error: RefCell::new(None),
|
cx.insert_error(error)
|
||||||
_id: current_scope_id()
|
} else {
|
||||||
.expect("Cannot create an error boundary outside of a component's scope."),
|
tracing::error!(
|
||||||
}
|
"Tried to throw an error into an error boundary, but failed to locate a boundary: {:?}",
|
||||||
}
|
error
|
||||||
}
|
)
|
||||||
|
|
||||||
impl ErrorBoundary {
|
|
||||||
/// Create a new error boundary
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self::default()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new error boundary in the current scope
|
|
||||||
pub(crate) fn new_in_scope(scope: ScopeId) -> Self {
|
|
||||||
Self {
|
|
||||||
inner: Rc::new(ErrorBoundaryInner {
|
|
||||||
error: RefCell::new(None),
|
|
||||||
_id: scope,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Push an error into this Error Boundary
|
|
||||||
pub fn insert_error(&self, scope: ScopeId, error: impl Debug + 'static, backtrace: Backtrace) {
|
|
||||||
self.inner.error.replace(Some(CapturedError {
|
|
||||||
error: Box::new(error),
|
|
||||||
scope,
|
|
||||||
backtrace,
|
|
||||||
}));
|
|
||||||
if self.inner._id != ScopeId::ROOT {
|
|
||||||
self.inner._id.needs_update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Take any error that has been captured by this error boundary
|
|
||||||
pub fn take_error(&self) -> Option<CapturedError> {
|
|
||||||
self.inner.error.take()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait to allow results to be thrown upwards to the nearest Error Boundary
|
|
||||||
///
|
|
||||||
/// The canonical way of using this trait is to throw results from hooks, aborting rendering
|
|
||||||
/// through question mark syntax. The throw method returns an option that evaluates to None
|
|
||||||
/// if there is an error, injecting the error to the nearest error boundary.
|
|
||||||
///
|
|
||||||
/// If the value is `Ok`, then throw returns the value, not aborting the rendering process.
|
|
||||||
///
|
|
||||||
/// The call stack is saved for this component and provided to the error boundary
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use dioxus::prelude::*;
|
|
||||||
///
|
|
||||||
/// #[component]
|
|
||||||
/// fn app(count: String) -> Element {
|
|
||||||
/// let count: i32 = count.parse().throw()?;
|
|
||||||
///
|
|
||||||
/// rsx! {
|
|
||||||
/// div { "Count {count}" }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub trait Throw<S = ()>: Sized {
|
|
||||||
/// The value that will be returned in if the given value is `Ok`.
|
|
||||||
type Out;
|
|
||||||
|
|
||||||
/// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary.
|
|
||||||
///
|
|
||||||
/// If the value is `Ok`, then throw returns the value, not aborting the rendering process.
|
|
||||||
///
|
|
||||||
/// The call stack is saved for this component and provided to the error boundary
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// Note that you can also manually throw errors using the throw method on `ScopeState` directly,
|
|
||||||
/// which is what this trait shells out to.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use dioxus::prelude::*;
|
|
||||||
///
|
|
||||||
/// #[component]
|
|
||||||
/// fn app( count: String) -> Element {
|
|
||||||
/// let count: i32 = count.parse().throw()?;
|
|
||||||
///
|
|
||||||
/// rsx! {
|
|
||||||
/// div { "Count {count}" }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
fn throw(self) -> Option<Self::Out>;
|
|
||||||
|
|
||||||
/// Returns an option that evaluates to None if there is an error, injecting the error to the nearest error boundary.
|
|
||||||
///
|
|
||||||
/// If the value is `Ok`, then throw returns the value, not aborting the rendering process.
|
|
||||||
///
|
|
||||||
/// The call stack is saved for this component and provided to the error boundary
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// Note that you can also manually throw errors using the throw method on `ScopeState` directly,
|
|
||||||
/// which is what this trait shells out to.
|
|
||||||
///
|
|
||||||
///
|
|
||||||
/// ```rust
|
|
||||||
/// use dioxus::prelude::*;
|
|
||||||
///
|
|
||||||
/// #[component]
|
|
||||||
/// fn app( count: String) -> Element {
|
|
||||||
/// let count: i32 = count.parse().throw()?;
|
|
||||||
///
|
|
||||||
/// rsx! {
|
|
||||||
/// div { "Count {count}" }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
fn throw_with<D: Debug + 'static>(self, e: impl FnOnce() -> D) -> Option<Self::Out> {
|
|
||||||
self.throw().or_else(|| throw_error(e()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn throw_error<T>(e: impl Debug + 'static) -> Option<T> {
|
|
||||||
if let Some(cx) = try_consume_context::<ErrorBoundary>() {
|
|
||||||
match current_scope_id() {
|
|
||||||
Some(id) => cx.insert_error(id, Box::new(e), Backtrace::capture()),
|
|
||||||
None => {
|
|
||||||
tracing::error!("Cannot throw error outside of a component's scope.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// We call clone on any errors that can be owned out of a reference
|
|
||||||
impl<'a, T, O: Debug + 'static, E: ToOwned<Owned = O>> Throw for &'a Result<T, E> {
|
|
||||||
type Out = &'a T;
|
|
||||||
|
|
||||||
fn throw(self) -> Option<Self::Out> {
|
|
||||||
match self {
|
|
||||||
Ok(t) => Some(t),
|
|
||||||
Err(e) => throw_error(e.to_owned()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn throw_with<D: Debug + 'static>(self, err: impl FnOnce() -> D) -> Option<Self::Out> {
|
|
||||||
match self {
|
|
||||||
Ok(t) => Some(t),
|
|
||||||
Err(_e) => throw_error(err()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Or just throw errors we know about
|
|
||||||
impl<T, E: Debug + 'static> Throw for Result<T, E> {
|
|
||||||
type Out = T;
|
|
||||||
|
|
||||||
fn throw(self) -> Option<T> {
|
|
||||||
match self {
|
|
||||||
Ok(t) => Some(t),
|
|
||||||
Err(e) => throw_error(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn throw_with<D: Debug + 'static>(self, error: impl FnOnce() -> D) -> Option<Self::Out> {
|
|
||||||
self.ok().or_else(|| throw_error(error()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Or just throw errors we know about
|
|
||||||
impl<T> Throw for Option<T> {
|
|
||||||
type Out = T;
|
|
||||||
|
|
||||||
fn throw(self) -> Option<T> {
|
|
||||||
self.or_else(|| throw_error("Attempted to unwrap a None value."))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn throw_with<D: Debug + 'static>(self, error: impl FnOnce() -> D) -> Option<Self::Out> {
|
|
||||||
self.or_else(|| throw_error(error()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ErrorHandler(Rc<dyn Fn(CapturedError) -> Element>);
|
pub struct ErrorHandler(Rc<dyn Fn(ErrorContext) -> Element>);
|
||||||
impl<F: Fn(CapturedError) -> Element + 'static> From<F> for ErrorHandler {
|
impl<F: Fn(ErrorContext) -> Element + 'static> From<F> for ErrorHandler {
|
||||||
fn from(value: F) -> Self {
|
fn from(value: F) -> Self {
|
||||||
Self(Rc::new(value))
|
Self(Rc::new(value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn default_handler(error: CapturedError) -> Element {
|
|
||||||
|
fn default_handler(errors: ErrorContext) -> Element {
|
||||||
static TEMPLATE: Template = Template {
|
static TEMPLATE: Template = Template {
|
||||||
name: "error_handle.rs:42:5:884",
|
name: "error_handle.rs:42:5:884",
|
||||||
roots: &[TemplateNode::Element {
|
roots: &[TemplateNode::Element {
|
||||||
tag: "pre",
|
tag: "div",
|
||||||
namespace: None,
|
namespace: None,
|
||||||
attrs: &[TemplateAttribute::Static {
|
attrs: &[TemplateAttribute::Static {
|
||||||
name: "color",
|
name: "color",
|
||||||
namespace: Some("style"),
|
namespace: Some("style"),
|
||||||
value: "red",
|
value: "red",
|
||||||
}],
|
}],
|
||||||
children: &[TemplateNode::DynamicText { id: 0usize }],
|
children: &[TemplateNode::Dynamic { id: 0usize }],
|
||||||
}],
|
}],
|
||||||
node_paths: &[&[0u8, 0u8]],
|
node_paths: &[&[0u8, 0u8]],
|
||||||
attr_paths: &[],
|
attr_paths: &[],
|
||||||
};
|
};
|
||||||
Some(VNode::new(
|
std::result::Result::Ok(VNode::new(
|
||||||
None,
|
None,
|
||||||
TEMPLATE,
|
TEMPLATE,
|
||||||
Box::new([error.to_string().into_dyn_node()]),
|
Box::new([errors
|
||||||
|
.errors()
|
||||||
|
.iter()
|
||||||
|
.map(|e| {
|
||||||
|
static TEMPLATE: Template = Template {
|
||||||
|
name: "error_handle.rs:43:5:884",
|
||||||
|
roots: &[TemplateNode::Element {
|
||||||
|
tag: "pre",
|
||||||
|
namespace: None,
|
||||||
|
attrs: &[],
|
||||||
|
children: &[TemplateNode::Dynamic { id: 0usize }],
|
||||||
|
}],
|
||||||
|
node_paths: &[&[0u8, 0u8]],
|
||||||
|
attr_paths: &[],
|
||||||
|
};
|
||||||
|
VNode::new(
|
||||||
|
None,
|
||||||
|
TEMPLATE,
|
||||||
|
Box::new([e.to_string().into_dyn_node()]),
|
||||||
|
Default::default(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_dyn_node()]),
|
||||||
Default::default(),
|
Default::default(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -433,9 +687,7 @@ impl<
|
||||||
{
|
{
|
||||||
pub fn build(self) -> ErrorBoundaryProps {
|
pub fn build(self) -> ErrorBoundaryProps {
|
||||||
let (children, handle_error) = self.fields;
|
let (children, handle_error) = self.fields;
|
||||||
let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, || {
|
let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
|
||||||
::core::default::Default::default()
|
|
||||||
});
|
|
||||||
let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || {
|
let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || {
|
||||||
ErrorHandler(Rc::new(default_handler))
|
ErrorHandler(Rc::new(default_handler))
|
||||||
});
|
});
|
||||||
|
@ -445,23 +697,45 @@ impl<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Create a new error boundary component.
|
|
||||||
|
/// Create a new error boundary component that catches any errors thrown from child components
|
||||||
///
|
///
|
||||||
/// ## Details
|
/// ## Details
|
||||||
///
|
///
|
||||||
/// Error boundaries handle errors within a specific part of your application. Any errors passed in a child with [`Throw`] will be caught by the nearest error boundary.
|
/// Error boundaries handle errors within a specific part of your application. Any errors passed up from a child will be caught by the nearest error boundary.
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust, no_run
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// # fn ThrowsError() -> Element { unimplemented!() }
|
/// fn App() -> Element {
|
||||||
/// rsx! {
|
/// rsx! {
|
||||||
/// ErrorBoundary {
|
/// ErrorBoundary {
|
||||||
/// handle_error: |error| rsx! { "Oops, we encountered an error. Please report {error} to the developer of this application" },
|
/// handle_error: |errors: ErrorContext| rsx! { "Oops, we encountered an error. Please report {errors:?} to the developer of this application" },
|
||||||
/// ThrowsError {}
|
/// Counter {
|
||||||
|
/// multiplier: "1234"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
/// }
|
/// }
|
||||||
/// };
|
/// }
|
||||||
|
///
|
||||||
|
/// #[component]
|
||||||
|
/// fn Counter(multiplier: String) -> Element {
|
||||||
|
/// // You can bubble up errors with `?` inside components
|
||||||
|
/// let multiplier_parsed = multiplier.parse::<usize>()?;
|
||||||
|
/// let mut count = use_signal(|| multiplier_parsed);
|
||||||
|
/// rsx! {
|
||||||
|
/// button {
|
||||||
|
/// // Or inside event handlers
|
||||||
|
/// onclick: move |_| {
|
||||||
|
/// let multiplier_parsed = multiplier.parse::<usize>()?;
|
||||||
|
/// *count.write() *= multiplier_parsed;
|
||||||
|
/// Ok(())
|
||||||
|
/// },
|
||||||
|
/// "{count}x{multiplier}"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
/// ## Usage
|
/// ## Usage
|
||||||
|
@ -472,9 +746,9 @@ impl<
|
||||||
#[allow(non_upper_case_globals, non_snake_case)]
|
#[allow(non_upper_case_globals, non_snake_case)]
|
||||||
pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
|
pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
|
||||||
let error_boundary = use_error_boundary();
|
let error_boundary = use_error_boundary();
|
||||||
match error_boundary.take_error() {
|
let errors = error_boundary.errors();
|
||||||
Some(error) => (props.handle_error.0)(error),
|
if errors.is_empty() {
|
||||||
None => Some({
|
std::result::Result::Ok({
|
||||||
static TEMPLATE: Template = Template {
|
static TEMPLATE: Template = Template {
|
||||||
name: "examples/error_handle.rs:81:17:2342",
|
name: "examples/error_handle.rs:81:17:2342",
|
||||||
roots: &[TemplateNode::Dynamic { id: 0usize }],
|
roots: &[TemplateNode::Dynamic { id: 0usize }],
|
||||||
|
@ -487,6 +761,8 @@ pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element {
|
||||||
Box::new([(props.children).into_dyn_node()]),
|
Box::new([(props.children).into_dyn_node()]),
|
||||||
Default::default(),
|
Default::default(),
|
||||||
)
|
)
|
||||||
}),
|
})
|
||||||
|
} else {
|
||||||
|
(props.handle_error.0)(error_boundary.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,7 +209,7 @@ pub struct Callback<Args = (), Ret = ()> {
|
||||||
/// fn Child(onclick: EventHandler<MouseEvent>) -> Element {
|
/// fn Child(onclick: EventHandler<MouseEvent>) -> Element {
|
||||||
/// rsx!{
|
/// rsx!{
|
||||||
/// button {
|
/// button {
|
||||||
/// // Diffing Child will not rerun this component, it will just update the EventHandler in place so that if this callback is called, it will run the latest version of the callback
|
/// // Diffing Child will not rerun this component, it will just update the callback in place so that if this callback is called, it will run the latest version of the callback
|
||||||
/// onclick: move |evt| onclick(evt),
|
/// onclick: move |evt| onclick(evt),
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
|
@ -270,10 +270,10 @@ impl<Ret> SpawnIfAsync<(), Ret> for Ret {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Support for FnMut -> async { anything } for the unit return type
|
// Support for FnMut -> async { unit } for the unit return type
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub struct AsyncMarker<O>(PhantomData<O>);
|
pub struct AsyncMarker;
|
||||||
impl<F: std::future::Future<Output = O> + 'static, O> SpawnIfAsync<AsyncMarker<O>, ()> for F {
|
impl<F: std::future::Future<Output = ()> + 'static> SpawnIfAsync<AsyncMarker> for F {
|
||||||
fn spawn(self) {
|
fn spawn(self) {
|
||||||
crate::prelude::spawn(async move {
|
crate::prelude::spawn(async move {
|
||||||
self.await;
|
self.await;
|
||||||
|
@ -281,6 +281,34 @@ impl<F: std::future::Future<Output = O> + 'static, O> SpawnIfAsync<AsyncMarker<O
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Support for FnMut -> async { Result(()) } for the unit return type
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct AsyncResultMarker;
|
||||||
|
|
||||||
|
impl<T> SpawnIfAsync<AsyncResultMarker> for T
|
||||||
|
where
|
||||||
|
T: std::future::Future<Output = crate::Result<()>> + 'static,
|
||||||
|
{
|
||||||
|
#[inline]
|
||||||
|
fn spawn(self) {
|
||||||
|
crate::prelude::spawn(async move {
|
||||||
|
if let Err(err) = self.await {
|
||||||
|
crate::prelude::throw_error(err)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support for FnMut -> Result(()) for the unit return type
|
||||||
|
impl SpawnIfAsync<()> for crate::Result<()> {
|
||||||
|
#[inline]
|
||||||
|
fn spawn(self) {
|
||||||
|
if let Err(err) = self {
|
||||||
|
crate::prelude::throw_error(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We can't directly forward the marker because it would overlap with a bunch of other impls, so we wrap it in another type instead
|
// We can't directly forward the marker because it would overlap with a bunch of other impls, so we wrap it in another type instead
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub struct MarkerWrapper<T>(PhantomData<T>);
|
pub struct MarkerWrapper<T>(PhantomData<T>);
|
||||||
|
@ -299,6 +327,22 @@ impl<
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub struct UnitClosure<Marker>(PhantomData<Marker>);
|
||||||
|
|
||||||
|
// Closure can be created from FnMut -> async { () } or FnMut -> Ret
|
||||||
|
impl<
|
||||||
|
Function: FnMut() -> Spawn + 'static,
|
||||||
|
Spawn: SpawnIfAsync<Marker, Ret> + 'static,
|
||||||
|
Ret: 'static,
|
||||||
|
Marker,
|
||||||
|
> SuperFrom<Function, UnitClosure<Marker>> for Callback<(), Ret>
|
||||||
|
{
|
||||||
|
fn super_from(mut input: Function) -> Self {
|
||||||
|
Callback::new(move |()| input())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn closure_types_infer() {
|
fn closure_types_infer() {
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
|
@ -315,6 +359,11 @@ fn closure_types_infer() {
|
||||||
let callback: Callback<u32, ()> = Callback::new(|value: u32| async move {
|
let callback: Callback<u32, ()> = Callback::new(|value: u32| async move {
|
||||||
println!("{}", value);
|
println!("{}", value);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Unit closures shouldn't require an argument
|
||||||
|
let callback: Callback<(), ()> = Callback::super_from(|| async move {
|
||||||
|
println!("hello world");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -369,12 +418,12 @@ impl<Args: 'static, Ret: 'static> Callback<Args, Ret> {
|
||||||
/// This borrows the callback using a RefCell. Recursively calling a callback will cause a panic.
|
/// This borrows the callback using a RefCell. Recursively calling a callback will cause a panic.
|
||||||
pub fn call(&self, arguments: Args) -> Ret {
|
pub fn call(&self, arguments: Args) -> Ret {
|
||||||
if let Some(callback) = self.callback.read().as_ref() {
|
if let Some(callback) = self.callback.read().as_ref() {
|
||||||
Runtime::with(|rt| rt.scope_stack.borrow_mut().push(self.origin));
|
Runtime::with(|rt| rt.push_scope(self.origin));
|
||||||
let value = {
|
let value = {
|
||||||
let mut callback = callback.borrow_mut();
|
let mut callback = callback.borrow_mut();
|
||||||
callback(arguments)
|
callback(arguments)
|
||||||
};
|
};
|
||||||
Runtime::with(|rt| rt.scope_stack.borrow_mut().pop());
|
Runtime::with(|rt| rt.pop_scope());
|
||||||
value
|
value
|
||||||
} else {
|
} else {
|
||||||
panic!("Callback was manually dropped")
|
panic!("Callback was manually dropped")
|
||||||
|
|
|
@ -33,7 +33,7 @@ pub fn Fragment(cx: FragmentProps) -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq)]
|
#[derive(Clone, PartialEq)]
|
||||||
pub struct FragmentProps(Element);
|
pub struct FragmentProps(pub(crate) Element);
|
||||||
|
|
||||||
pub struct FragmentBuilder<const BUILT: bool>(Element);
|
pub struct FragmentBuilder<const BUILT: bool>(Element);
|
||||||
impl FragmentBuilder<false> {
|
impl FragmentBuilder<false> {
|
||||||
|
@ -66,7 +66,7 @@ impl<const A: bool> FragmentBuilder<A> {
|
||||||
/// ```rust
|
/// ```rust
|
||||||
/// # use dioxus::prelude::*;
|
/// # use dioxus::prelude::*;
|
||||||
/// fn app() -> Element {
|
/// fn app() -> Element {
|
||||||
/// rsx!{
|
/// rsx! {
|
||||||
/// CustomCard {
|
/// CustomCard {
|
||||||
/// h1 {}
|
/// h1 {}
|
||||||
/// p {}
|
/// p {}
|
||||||
|
@ -87,7 +87,7 @@ impl<const A: bool> FragmentBuilder<A> {
|
||||||
impl Properties for FragmentProps {
|
impl Properties for FragmentProps {
|
||||||
type Builder = FragmentBuilder<false>;
|
type Builder = FragmentBuilder<false>;
|
||||||
fn builder() -> Self::Builder {
|
fn builder() -> Self::Builder {
|
||||||
FragmentBuilder(None)
|
FragmentBuilder(VNode::empty())
|
||||||
}
|
}
|
||||||
fn memoize(&mut self, new: &Self) -> bool {
|
fn memoize(&mut self, new: &Self) -> bool {
|
||||||
let equal = self == new;
|
let equal = self == new;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{runtime::Runtime, Element, ScopeId, Task};
|
use crate::{innerlude::SuspendedFuture, runtime::Runtime, CapturedError, Element, ScopeId, Task};
|
||||||
use futures_util::Future;
|
use std::future::Future;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
/// Get the current scope id
|
/// Get the current scope id
|
||||||
|
@ -13,6 +13,29 @@ pub fn vdom_is_rendering() -> bool {
|
||||||
Runtime::with(|rt| rt.rendering.get()).unwrap_or_default()
|
Runtime::with(|rt| rt.rendering.get()).unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Throw a [`CapturedError`] into the current scope. The error will bubble up to the nearest [`crate::prelude::ErrorBoundary()`] or the root of the app.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn Component() -> Element {
|
||||||
|
/// let request = spawn(async move {
|
||||||
|
/// match reqwest::get("https://api.example.com").await {
|
||||||
|
/// Ok(_) => todo!(),
|
||||||
|
/// // You can explicitly throw an error into a scope with throw_error
|
||||||
|
/// Err(err) => ScopeId::APP.throw_error(err)
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// todo!()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn throw_error(error: impl Into<CapturedError> + 'static) {
|
||||||
|
current_scope_id()
|
||||||
|
.expect("to be in a dioxus runtime")
|
||||||
|
.throw_error(error)
|
||||||
|
}
|
||||||
|
|
||||||
/// Consume context from the current scope
|
/// Consume context from the current scope
|
||||||
pub fn try_consume_context<T: 'static + Clone>() -> Option<T> {
|
pub fn try_consume_context<T: 'static + Clone>() -> Option<T> {
|
||||||
Runtime::with_current_scope(|cx| cx.consume_context::<T>()).flatten()
|
Runtime::with_current_scope(|cx| cx.consume_context::<T>()).flatten()
|
||||||
|
@ -52,8 +75,9 @@ pub fn provide_root_context<T: 'static + Clone>(value: T) -> T {
|
||||||
|
|
||||||
/// Suspended the current component on a specific task and then return None
|
/// Suspended the current component on a specific task and then return None
|
||||||
pub fn suspend(task: Task) -> Element {
|
pub fn suspend(task: Task) -> Element {
|
||||||
Runtime::with_current_scope(|cx| cx.suspend(task));
|
Err(crate::innerlude::RenderError::Suspended(
|
||||||
None
|
SuspendedFuture::new(task),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start a new future on the same thread as the rest of the VirtualDom.
|
/// Start a new future on the same thread as the rest of the VirtualDom.
|
||||||
|
@ -191,7 +215,7 @@ pub fn remove_future(id: Task) {
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust, no_run
|
||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
///
|
///
|
||||||
/// // prints a greeting on the initial render
|
/// // prints a greeting on the initial render
|
||||||
|
@ -202,7 +226,7 @@ pub fn remove_future(id: Task) {
|
||||||
///
|
///
|
||||||
/// # Custom Hook Example
|
/// # Custom Hook Example
|
||||||
///
|
///
|
||||||
/// ```rust
|
/// ```rust, no_run
|
||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
///
|
///
|
||||||
/// pub struct InnerCustomState(usize);
|
/// pub struct InnerCustomState(usize);
|
||||||
|
@ -377,21 +401,6 @@ pub fn after_render(f: impl FnMut() + 'static) {
|
||||||
Runtime::with_current_scope(|cx| cx.push_after_render(f));
|
Runtime::with_current_scope(|cx| cx.push_after_render(f));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Wait for the next render to complete
|
|
||||||
///
|
|
||||||
/// This is useful if you've just triggered an update and want to wait for it to finish before proceeding with valid
|
|
||||||
/// DOM nodes.
|
|
||||||
///
|
|
||||||
/// Effects rely on this to ensure that they only run effects after the DOM has been updated. Without wait_for_next_render effects
|
|
||||||
/// are run immediately before diffing the DOM, which causes all sorts of out-of-sync weirdness.
|
|
||||||
pub async fn wait_for_next_render() {
|
|
||||||
// Wait for the flush lock to be available
|
|
||||||
// We release it immediately, so it's impossible for the lock to be held longer than this function
|
|
||||||
Runtime::with(|rt| rt.render_signal.subscribe())
|
|
||||||
.unwrap()
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Use a hook with a cleanup function
|
/// Use a hook with a cleanup function
|
||||||
pub fn use_hook_with_cleanup<T: Clone + 'static>(
|
pub fn use_hook_with_cleanup<T: Clone + 'static>(
|
||||||
hook: impl FnOnce() -> T,
|
hook: impl FnOnce() -> T,
|
||||||
|
|
|
@ -16,12 +16,14 @@ mod mutations;
|
||||||
mod nodes;
|
mod nodes;
|
||||||
mod properties;
|
mod properties;
|
||||||
mod reactive_context;
|
mod reactive_context;
|
||||||
mod render_signal;
|
mod render_error;
|
||||||
|
mod root_wrapper;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod scope_arena;
|
mod scope_arena;
|
||||||
mod scope_context;
|
mod scope_context;
|
||||||
mod scopes;
|
mod scopes;
|
||||||
|
mod suspense;
|
||||||
mod tasks;
|
mod tasks;
|
||||||
mod virtual_dom;
|
mod virtual_dom;
|
||||||
|
|
||||||
|
@ -44,16 +46,18 @@ pub(crate) mod innerlude {
|
||||||
pub use crate::nodes::*;
|
pub use crate::nodes::*;
|
||||||
pub use crate::properties::*;
|
pub use crate::properties::*;
|
||||||
pub use crate::reactive_context::*;
|
pub use crate::reactive_context::*;
|
||||||
|
pub use crate::render_error::*;
|
||||||
pub use crate::runtime::{Runtime, RuntimeGuard};
|
pub use crate::runtime::{Runtime, RuntimeGuard};
|
||||||
pub use crate::scheduler::*;
|
pub use crate::scheduler::*;
|
||||||
pub use crate::scopes::*;
|
pub use crate::scopes::*;
|
||||||
|
pub use crate::suspense::*;
|
||||||
pub use crate::tasks::*;
|
pub use crate::tasks::*;
|
||||||
pub use crate::virtual_dom::*;
|
pub use crate::virtual_dom::*;
|
||||||
|
|
||||||
/// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`ScopeId`] or [`ScopeState`].
|
/// An [`Element`] is a possibly-none [`VNode`] created by calling `render` on [`ScopeId`] or [`ScopeState`].
|
||||||
///
|
///
|
||||||
/// An Errored [`Element`] will propagate the error to the nearest error boundary.
|
/// An Errored [`Element`] will propagate the error to the nearest error boundary.
|
||||||
pub type Element = Option<VNode>;
|
pub type Element = std::result::Result<VNode, RenderError>;
|
||||||
|
|
||||||
/// A [`Component`] is a function that takes [`Properties`] and returns an [`Element`].
|
/// A [`Component`] is a function that takes [`Properties`] and returns an [`Element`].
|
||||||
pub type Component<P = ()> = fn(P) -> Element;
|
pub type Component<P = ()> = fn(P) -> Element;
|
||||||
|
@ -63,9 +67,9 @@ pub use crate::innerlude::{
|
||||||
fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering,
|
fc_to_builder, generation, schedule_update, schedule_update_any, use_hook, vdom_is_rendering,
|
||||||
AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode,
|
AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode,
|
||||||
Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, MarkerWrapper, Mutation,
|
Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, MarkerWrapper, Mutation,
|
||||||
Mutations, NoOpMutations, Properties, RenderReturn, Runtime, ScopeId, ScopeState, SpawnIfAsync,
|
Mutations, NoOpMutations, Ok, Properties, RenderReturn, Result, Runtime, ScopeId, ScopeState,
|
||||||
Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder,
|
SpawnIfAsync, Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner,
|
||||||
VText, VirtualDom, WriteMutations,
|
VPlaceholder, VText, VirtualDom, WriteMutations,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The purpose of this module is to alleviate imports of many common types
|
/// The purpose of this module is to alleviate imports of many common types
|
||||||
|
@ -76,12 +80,14 @@ pub mod prelude {
|
||||||
consume_context, consume_context_from_scope, current_owner, current_scope_id,
|
consume_context, consume_context_from_scope, current_owner, current_scope_id,
|
||||||
fc_to_builder, generation, has_context, needs_update, needs_update_any, parent_scope,
|
fc_to_builder, generation, has_context, needs_update, needs_update_any, parent_scope,
|
||||||
provide_context, provide_root_context, queue_effect, remove_future, schedule_update,
|
provide_context, provide_root_context, queue_effect, remove_future, schedule_update,
|
||||||
schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, try_consume_context,
|
schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, throw_error,
|
||||||
use_after_render, use_before_render, use_drop, use_error_boundary, use_hook,
|
try_consume_context, use_after_render, use_before_render, use_drop, use_error_boundary,
|
||||||
use_hook_with_cleanup, wait_for_next_render, with_owner, AnyValue, Attribute, Callback,
|
use_hook, use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, CapturedError,
|
||||||
Component, ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment,
|
Component, ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event,
|
||||||
HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties,
|
EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode,
|
||||||
ReactiveContext, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, SuperInto, Task,
|
OptionStringFromMarker, Properties, ReactiveContext, RenderError, Runtime, RuntimeGuard,
|
||||||
Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, VirtualDom,
|
ScopeId, ScopeState, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary,
|
||||||
|
SuspenseBoundaryProps, SuspenseContext, SuspenseExtension, Task, Template,
|
||||||
|
TemplateAttribute, TemplateNode, VNode, VNodeInner, VirtualDom,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use crate::innerlude::VProps;
|
use crate::innerlude::{RenderError, VProps};
|
||||||
use crate::{any_props::BoxedAnyProps, innerlude::ScopeState};
|
use crate::{any_props::BoxedAnyProps, innerlude::ScopeState};
|
||||||
use crate::{arena::ElementId, Element, Event};
|
use crate::{arena::ElementId, Element, Event};
|
||||||
use crate::{
|
use crate::{
|
||||||
innerlude::{ElementRef, EventHandler, MountId},
|
innerlude::{ElementRef, EventHandler, MountId},
|
||||||
properties::ComponentFunction,
|
properties::ComponentFunction,
|
||||||
};
|
};
|
||||||
use crate::{Properties, VirtualDom};
|
use crate::{Properties, ScopeId, VirtualDom};
|
||||||
use core::panic;
|
use core::panic;
|
||||||
use std::ops::Deref;
|
use std::ops::{Deref, DerefMut};
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::vec;
|
use std::vec;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -21,28 +21,49 @@ pub type TemplateId = &'static str;
|
||||||
/// The actual state of the component's most recent computation
|
/// The actual state of the component's most recent computation
|
||||||
///
|
///
|
||||||
/// If the component returned early (e.g. `return None`), this will be Aborted(None)
|
/// If the component returned early (e.g. `return None`), this will be Aborted(None)
|
||||||
pub enum RenderReturn {
|
#[derive(Debug)]
|
||||||
/// A currently-available element
|
pub struct RenderReturn {
|
||||||
Ready(VNode),
|
/// The node that was rendered
|
||||||
|
pub(crate) node: Element,
|
||||||
|
}
|
||||||
|
|
||||||
/// The component aborted rendering early. It might've thrown an error.
|
impl From<RenderReturn> for VNode {
|
||||||
///
|
fn from(val: RenderReturn) -> Self {
|
||||||
/// In its place we've produced a placeholder to locate its spot in the dom when it recovers.
|
match val.node {
|
||||||
Aborted(VNode),
|
Ok(node) => node,
|
||||||
|
Err(RenderError::Aborted(e)) => e.render,
|
||||||
|
Err(RenderError::Suspended(fut)) => fut.placeholder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Element> for RenderReturn {
|
||||||
|
fn from(node: Element) -> Self {
|
||||||
|
RenderReturn { node }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for RenderReturn {
|
impl Clone for RenderReturn {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
match self {
|
match &self.node {
|
||||||
RenderReturn::Ready(node) => RenderReturn::Ready(node.clone_mounted()),
|
Ok(node) => RenderReturn {
|
||||||
RenderReturn::Aborted(node) => RenderReturn::Aborted(node.clone_mounted()),
|
node: Ok(node.clone_mounted()),
|
||||||
|
},
|
||||||
|
Err(RenderError::Aborted(err)) => RenderReturn {
|
||||||
|
node: Err(RenderError::Aborted(err.clone_mounted())),
|
||||||
|
},
|
||||||
|
Err(RenderError::Suspended(fut)) => RenderReturn {
|
||||||
|
node: Err(RenderError::Suspended(fut.clone_mounted())),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RenderReturn {
|
impl Default for RenderReturn {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
RenderReturn::Aborted(VNode::placeholder())
|
RenderReturn {
|
||||||
|
node: Ok(VNode::placeholder()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -50,8 +71,20 @@ impl Deref for RenderReturn {
|
||||||
type Target = VNode;
|
type Target = VNode;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
match self {
|
match &self.node {
|
||||||
RenderReturn::Ready(node) | RenderReturn::Aborted(node) => node,
|
Ok(node) => node,
|
||||||
|
Err(RenderError::Aborted(err)) => &err.render,
|
||||||
|
Err(RenderError::Suspended(fut)) => &fut.placeholder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerefMut for RenderReturn {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
match &mut self.node {
|
||||||
|
Ok(node) => node,
|
||||||
|
Err(RenderError::Aborted(err)) => &mut err.render,
|
||||||
|
Err(RenderError::Suspended(fut)) => &mut fut.placeholder,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -149,6 +182,12 @@ impl Clone for VNode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for VNode {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::placeholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Drop for VNode {
|
impl Drop for VNode {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
// FIXME:
|
// FIXME:
|
||||||
|
@ -197,31 +236,7 @@ impl VNode {
|
||||||
|
|
||||||
/// Create a template with no nodes that will be skipped over during diffing
|
/// Create a template with no nodes that will be skipped over during diffing
|
||||||
pub fn empty() -> Element {
|
pub fn empty() -> Element {
|
||||||
use std::cell::OnceCell;
|
Ok(Self::default())
|
||||||
// We can reuse all placeholders across the same thread to save memory
|
|
||||||
thread_local! {
|
|
||||||
static EMPTY_VNODE: OnceCell<Rc<VNodeInner>> = const { OnceCell::new() };
|
|
||||||
}
|
|
||||||
let vnode = EMPTY_VNODE.with(|cell| {
|
|
||||||
cell.get_or_init(move || {
|
|
||||||
Rc::new(VNodeInner {
|
|
||||||
key: None,
|
|
||||||
dynamic_nodes: Box::new([]),
|
|
||||||
dynamic_attrs: Box::new([]),
|
|
||||||
template: Cell::new(Template {
|
|
||||||
name: "packages/core/nodes.rs:180:0:0",
|
|
||||||
roots: &[],
|
|
||||||
node_paths: &[],
|
|
||||||
attr_paths: &[],
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.clone()
|
|
||||||
});
|
|
||||||
Some(Self {
|
|
||||||
vnode,
|
|
||||||
mount: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a template with a single placeholder node
|
/// Create a template with a single placeholder node
|
||||||
|
@ -240,7 +255,7 @@ impl VNode {
|
||||||
template: Cell::new(Template {
|
template: Cell::new(Template {
|
||||||
name: "packages/core/nodes.rs:198:0:0",
|
name: "packages/core/nodes.rs:198:0:0",
|
||||||
roots: &[TemplateNode::Dynamic { id: 0 }],
|
roots: &[TemplateNode::Dynamic { id: 0 }],
|
||||||
node_paths: &[&[]],
|
node_paths: &[&[0]],
|
||||||
attr_paths: &[],
|
attr_paths: &[],
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
@ -275,12 +290,9 @@ impl VNode {
|
||||||
///
|
///
|
||||||
/// Returns [`None`] if the root is actually a static node (Element/Text)
|
/// Returns [`None`] if the root is actually a static node (Element/Text)
|
||||||
pub fn dynamic_root(&self, idx: usize) -> Option<&DynamicNode> {
|
pub fn dynamic_root(&self, idx: usize) -> Option<&DynamicNode> {
|
||||||
match &self.template.get().roots[idx] {
|
self.template.get().roots[idx]
|
||||||
TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => None,
|
.dynamic_id()
|
||||||
TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => {
|
.map(|id| &self.dynamic_nodes[id])
|
||||||
Some(&self.dynamic_nodes[*id])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the mounted id for a dynamic node index
|
/// Get the mounted id for a dynamic node index
|
||||||
|
@ -427,9 +439,14 @@ impl Template {
|
||||||
/// There's no point in saving templates that are completely dynamic, since they'll be recreated every time anyway.
|
/// There's no point in saving templates that are completely dynamic, since they'll be recreated every time anyway.
|
||||||
pub fn is_completely_dynamic(&self) -> bool {
|
pub fn is_completely_dynamic(&self) -> bool {
|
||||||
use TemplateNode::*;
|
use TemplateNode::*;
|
||||||
self.roots
|
self.roots.iter().all(|root| matches!(root, Dynamic { .. }))
|
||||||
.iter()
|
}
|
||||||
.all(|root| matches!(root, Dynamic { .. } | DynamicText { .. }))
|
|
||||||
|
/// Get a unique id for this template. If the id between two templates are different, the contents of the template may be different.
|
||||||
|
pub fn id(&self) -> usize {
|
||||||
|
// We compare the template name by pointer so that the id is different after hot reloading even if the name is the same
|
||||||
|
let ptr: *const str = self.name;
|
||||||
|
ptr as *const () as usize
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Iterate over the attribute paths in order along with the original indexes for each path
|
/// Iterate over the attribute paths in order along with the original indexes for each path
|
||||||
|
@ -447,6 +464,20 @@ impl Template {
|
||||||
sort_bfo(self.attr_paths).into_iter()
|
sort_bfo(self.attr_paths).into_iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Iterate over the node paths in order along with the original indexes for each path
|
||||||
|
pub(crate) fn breadth_first_node_paths(&self) -> impl Iterator<Item = (usize, &'static [u8])> {
|
||||||
|
// In release mode, hot reloading is disabled and everything is in breadth first order already
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
self.node_paths.iter().copied().enumerate()
|
||||||
|
}
|
||||||
|
// If we are in debug mode, hot reloading may have messed up the order of the paths. We need to sort them
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
sort_bfo(self.node_paths).into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A statically known node in a layout.
|
/// A statically known node in a layout.
|
||||||
|
@ -500,14 +531,6 @@ pub enum TemplateNode {
|
||||||
/// The index of the dynamic node in the VNode's dynamic_nodes list
|
/// The index of the dynamic node in the VNode's dynamic_nodes list
|
||||||
id: usize,
|
id: usize,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// This template node is known to be some text, but needs to be created at runtime
|
|
||||||
///
|
|
||||||
/// This is separate from the pure Dynamic variant for various optimizations
|
|
||||||
DynamicText {
|
|
||||||
/// The index of the dynamic node in the VNode's dynamic_nodes list
|
|
||||||
id: usize,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TemplateNode {
|
impl TemplateNode {
|
||||||
|
@ -515,7 +538,7 @@ impl TemplateNode {
|
||||||
pub fn dynamic_id(&self) -> Option<usize> {
|
pub fn dynamic_id(&self) -> Option<usize> {
|
||||||
use TemplateNode::*;
|
use TemplateNode::*;
|
||||||
match self {
|
match self {
|
||||||
Dynamic { id } | DynamicText { id } => Some(*id),
|
Dynamic { id } => Some(*id),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -575,9 +598,20 @@ pub struct VComponent {
|
||||||
/// It is possible that components get folded at compile time, so these shouldn't be really used as a key
|
/// It is possible that components get folded at compile time, so these shouldn't be really used as a key
|
||||||
pub(crate) render_fn: TypeId,
|
pub(crate) render_fn: TypeId,
|
||||||
|
|
||||||
|
/// The props for this component
|
||||||
pub(crate) props: BoxedAnyProps,
|
pub(crate) props: BoxedAnyProps,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Clone for VComponent {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
name: self.name,
|
||||||
|
render_fn: self.render_fn,
|
||||||
|
props: self.props.duplicate(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl VComponent {
|
impl VComponent {
|
||||||
/// Create a new [`VComponent`] variant
|
/// Create a new [`VComponent`] variant
|
||||||
pub fn new<P, M: 'static>(
|
pub fn new<P, M: 'static>(
|
||||||
|
@ -603,6 +637,24 @@ impl VComponent {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the [`ScopeId`] this node is mounted to if it's mounted
|
||||||
|
///
|
||||||
|
/// This is useful for rendering nodes outside of the VirtualDom, such as in SSR
|
||||||
|
///
|
||||||
|
/// Returns [`None`] if the node is not mounted
|
||||||
|
pub fn mounted_scope_id(
|
||||||
|
&self,
|
||||||
|
dynamic_node_index: usize,
|
||||||
|
vnode: &VNode,
|
||||||
|
dom: &VirtualDom,
|
||||||
|
) -> Option<ScopeId> {
|
||||||
|
let mount = vnode.mount.get().as_usize()?;
|
||||||
|
|
||||||
|
let scope_id = dom.mounts.get(mount)?.mounted_dynamic_nodes[dynamic_node_index];
|
||||||
|
|
||||||
|
Some(ScopeId(scope_id))
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the scope this node is mounted to if it's mounted
|
/// Get the scope this node is mounted to if it's mounted
|
||||||
///
|
///
|
||||||
/// This is useful for rendering nodes outside of the VirtualDom, such as in SSR
|
/// This is useful for rendering nodes outside of the VirtualDom, such as in SSR
|
||||||
|
@ -844,9 +896,7 @@ impl<T: Any + PartialEq + 'static> AnyValue for T {
|
||||||
|
|
||||||
/// A trait that allows various items to be converted into a dynamic node for the rsx macro
|
/// A trait that allows various items to be converted into a dynamic node for the rsx macro
|
||||||
pub trait IntoDynNode<A = ()> {
|
pub trait IntoDynNode<A = ()> {
|
||||||
/// Consume this item along with a scopestate and produce a DynamicNode
|
/// Consume this item and produce a DynamicNode
|
||||||
///
|
|
||||||
/// You can use the bump alloactor of the scopestate to creat the dynamic node
|
|
||||||
fn into_dyn_node(self) -> DynamicNode;
|
fn into_dyn_node(self) -> DynamicNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -860,13 +910,11 @@ impl IntoDynNode for VNode {
|
||||||
DynamicNode::Fragment(vec![self])
|
DynamicNode::Fragment(vec![self])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDynNode for DynamicNode {
|
impl IntoDynNode for DynamicNode {
|
||||||
fn into_dyn_node(self) -> DynamicNode {
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: IntoDynNode> IntoDynNode for Option<T> {
|
impl<T: IntoDynNode> IntoDynNode for Option<T> {
|
||||||
fn into_dyn_node(self) -> DynamicNode {
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
match self {
|
match self {
|
||||||
|
@ -875,8 +923,23 @@ impl<T: IntoDynNode> IntoDynNode for Option<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDynNode for &Element {
|
impl IntoDynNode for &Element {
|
||||||
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
|
match self.as_ref() {
|
||||||
|
Ok(val) => val.into_dyn_node(),
|
||||||
|
_ => DynamicNode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl IntoDynNode for Element {
|
||||||
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
|
match self {
|
||||||
|
Ok(val) => val.into_dyn_node(),
|
||||||
|
_ => DynamicNode::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl IntoDynNode for &Option<VNode> {
|
||||||
fn into_dyn_node(self) -> DynamicNode {
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
match self.as_ref() {
|
match self.as_ref() {
|
||||||
Some(val) => val.clone().into_dyn_node(),
|
Some(val) => val.clone().into_dyn_node(),
|
||||||
|
@ -884,7 +947,6 @@ impl IntoDynNode for &Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDynNode for &str {
|
impl IntoDynNode for &str {
|
||||||
fn into_dyn_node(self) -> DynamicNode {
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
DynamicNode::Text(VText {
|
DynamicNode::Text(VText {
|
||||||
|
@ -892,13 +954,11 @@ impl IntoDynNode for &str {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDynNode for String {
|
impl IntoDynNode for String {
|
||||||
fn into_dyn_node(self) -> DynamicNode {
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
DynamicNode::Text(VText { value: self })
|
DynamicNode::Text(VText { value: self })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDynNode for Arguments<'_> {
|
impl IntoDynNode for Arguments<'_> {
|
||||||
fn into_dyn_node(self) -> DynamicNode {
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
DynamicNode::Text(VText {
|
DynamicNode::Text(VText {
|
||||||
|
@ -906,7 +966,6 @@ impl IntoDynNode for Arguments<'_> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoDynNode for &VNode {
|
impl IntoDynNode for &VNode {
|
||||||
fn into_dyn_node(self) -> DynamicNode {
|
fn into_dyn_node(self) -> DynamicNode {
|
||||||
DynamicNode::Fragment(vec![self.clone()])
|
DynamicNode::Fragment(vec![self.clone()])
|
||||||
|
@ -929,12 +988,20 @@ impl IntoVNode for &VNode {
|
||||||
impl IntoVNode for Element {
|
impl IntoVNode for Element {
|
||||||
fn into_vnode(self) -> VNode {
|
fn into_vnode(self) -> VNode {
|
||||||
match self {
|
match self {
|
||||||
Some(val) => val.into_vnode(),
|
Ok(val) => val.into_vnode(),
|
||||||
_ => VNode::empty().unwrap(),
|
_ => VNode::empty().unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl IntoVNode for &Element {
|
impl IntoVNode for &Element {
|
||||||
|
fn into_vnode(self) -> VNode {
|
||||||
|
match self {
|
||||||
|
Ok(val) => val.into_vnode(),
|
||||||
|
_ => VNode::empty().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl IntoVNode for Option<VNode> {
|
||||||
fn into_vnode(self) -> VNode {
|
fn into_vnode(self) -> VNode {
|
||||||
match self {
|
match self {
|
||||||
Some(val) => val.into_vnode(),
|
Some(val) => val.into_vnode(),
|
||||||
|
@ -942,6 +1009,30 @@ impl IntoVNode for &Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl IntoVNode for &Option<VNode> {
|
||||||
|
fn into_vnode(self) -> VNode {
|
||||||
|
match self.as_ref() {
|
||||||
|
Some(val) => val.clone().into_vnode(),
|
||||||
|
_ => VNode::empty().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl IntoVNode for Option<Element> {
|
||||||
|
fn into_vnode(self) -> VNode {
|
||||||
|
match self {
|
||||||
|
Some(val) => val.into_vnode(),
|
||||||
|
_ => VNode::empty().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl IntoVNode for &Option<Element> {
|
||||||
|
fn into_vnode(self) -> VNode {
|
||||||
|
match self.as_ref() {
|
||||||
|
Some(val) => val.clone().into_vnode(),
|
||||||
|
_ => VNode::empty().unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Note that we're using the E as a generic but this is never crafted anyways.
|
// Note that we're using the E as a generic but this is never crafted anyways.
|
||||||
pub struct FromNodeIterator;
|
pub struct FromNodeIterator;
|
||||||
|
|
52
packages/core/src/render_error.rs
Normal file
52
packages/core/src/render_error.rs
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
use std::fmt::{Debug, Display};
|
||||||
|
|
||||||
|
use crate::innerlude::*;
|
||||||
|
|
||||||
|
/// An error that can occur while rendering a component
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub enum RenderError {
|
||||||
|
/// The render function returned early
|
||||||
|
Aborted(CapturedError),
|
||||||
|
|
||||||
|
/// The component was suspended
|
||||||
|
Suspended(SuspendedFuture),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for RenderError {
|
||||||
|
fn default() -> Self {
|
||||||
|
struct RenderAbortedEarly;
|
||||||
|
impl Debug for RenderAbortedEarly {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("Render aborted early")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Display for RenderAbortedEarly {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str("Render aborted early")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for RenderAbortedEarly {}
|
||||||
|
Self::Aborted(RenderAbortedEarly.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for RenderError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Aborted(e) => write!(f, "Render aborted: {e}"),
|
||||||
|
Self::Suspended(e) => write!(f, "Component suspended: {e:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: std::error::Error + 'static> From<E> for RenderError {
|
||||||
|
fn from(e: E) -> Self {
|
||||||
|
Self::Aborted(CapturedError::from(e))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CapturedError> for RenderError {
|
||||||
|
fn from(e: CapturedError) -> Self {
|
||||||
|
RenderError::Aborted(e)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,62 +0,0 @@
|
||||||
//! TODO: We no longer run effects with async tasks. Effects are now their own type of task. We should remove this next breaking release.
|
|
||||||
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::future::Future;
|
|
||||||
use std::pin::Pin;
|
|
||||||
use std::rc::Rc;
|
|
||||||
use std::task::Context;
|
|
||||||
use std::task::Poll;
|
|
||||||
use std::task::Waker;
|
|
||||||
|
|
||||||
/// A signal is a message that can be sent to all listening tasks at once
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct RenderSignal {
|
|
||||||
wakers: Rc<RefCell<Vec<Rc<RefCell<RenderSignalFutureInner>>>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderSignal {
|
|
||||||
/// Send the signal to all listening tasks
|
|
||||||
pub fn send(&self) {
|
|
||||||
let mut wakers = self.wakers.borrow_mut();
|
|
||||||
for waker in wakers.drain(..) {
|
|
||||||
let mut inner = waker.borrow_mut();
|
|
||||||
inner.resolved = true;
|
|
||||||
if let Some(waker) = inner.waker.take() {
|
|
||||||
waker.wake();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a future that resolves when the signal is sent
|
|
||||||
pub fn subscribe(&self) -> RenderSignalFuture {
|
|
||||||
let inner = Rc::new(RefCell::new(RenderSignalFutureInner {
|
|
||||||
resolved: false,
|
|
||||||
waker: None,
|
|
||||||
}));
|
|
||||||
self.wakers.borrow_mut().push(inner.clone());
|
|
||||||
RenderSignalFuture { inner }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RenderSignalFutureInner {
|
|
||||||
resolved: bool,
|
|
||||||
waker: Option<Waker>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct RenderSignalFuture {
|
|
||||||
inner: Rc<RefCell<RenderSignalFutureInner>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Future for RenderSignalFuture {
|
|
||||||
type Output = ();
|
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
|
|
||||||
let mut inner = self.inner.borrow_mut();
|
|
||||||
if inner.resolved {
|
|
||||||
Poll::Ready(())
|
|
||||||
} else {
|
|
||||||
inner.waker = Some(cx.waker().clone());
|
|
||||||
Poll::Pending
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
41
packages/core/src/root_wrapper.rs
Normal file
41
packages/core/src/root_wrapper.rs
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
use crate::{prelude::*, properties::RootProps, DynamicNode, VComponent};
|
||||||
|
|
||||||
|
// We wrap the root scope in a component that renders it inside a default ErrorBoundary and SuspenseBoundary
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[allow(clippy::let_and_return)]
|
||||||
|
pub(crate) fn RootScopeWrapper(props: RootProps<VComponent>) -> Element {
|
||||||
|
static TEMPLATE: Template = Template {
|
||||||
|
name: "root_wrapper.rs:16:5:561",
|
||||||
|
roots: &[TemplateNode::Dynamic { id: 0usize }],
|
||||||
|
node_paths: &[&[0u8]],
|
||||||
|
attr_paths: &[],
|
||||||
|
};
|
||||||
|
Element::Ok(VNode::new(
|
||||||
|
None,
|
||||||
|
TEMPLATE,
|
||||||
|
Box::new([DynamicNode::Component(
|
||||||
|
fc_to_builder(ErrorBoundary)
|
||||||
|
.children(Element::Ok(VNode::new(
|
||||||
|
None,
|
||||||
|
TEMPLATE,
|
||||||
|
Box::new([DynamicNode::Component({
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
fc_to_builder(SuspenseBoundary)
|
||||||
|
.fallback(|_| Element::Ok(VNode::placeholder()))
|
||||||
|
.children(Ok(VNode::new(
|
||||||
|
None,
|
||||||
|
TEMPLATE,
|
||||||
|
Box::new([DynamicNode::Component(props.0)]),
|
||||||
|
Box::new([]),
|
||||||
|
)))
|
||||||
|
.build()
|
||||||
|
.into_vcomponent(SuspenseBoundary, "SuspenseBoundary")
|
||||||
|
})]),
|
||||||
|
Box::new([]),
|
||||||
|
)))
|
||||||
|
.build()
|
||||||
|
.into_vcomponent(ErrorBoundary, "ErrorBoundary"),
|
||||||
|
)]),
|
||||||
|
Box::new([]),
|
||||||
|
))
|
||||||
|
}
|
|
@ -1,13 +1,12 @@
|
||||||
use slotmap::DefaultKey;
|
use crate::innerlude::{DirtyTasks, Effect};
|
||||||
|
use crate::scope_context::SuspenseLocation;
|
||||||
use crate::innerlude::Effect;
|
|
||||||
use crate::{
|
use crate::{
|
||||||
innerlude::{LocalTask, SchedulerMsg},
|
innerlude::{LocalTask, SchedulerMsg},
|
||||||
render_signal::RenderSignal,
|
|
||||||
scope_context::Scope,
|
scope_context::Scope,
|
||||||
scopes::ScopeId,
|
scopes::ScopeId,
|
||||||
Task,
|
Task,
|
||||||
};
|
};
|
||||||
|
use slotmap::DefaultKey;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::{
|
use std::{
|
||||||
cell::{Cell, Ref, RefCell},
|
cell::{Cell, Ref, RefCell},
|
||||||
|
@ -25,6 +24,9 @@ pub struct Runtime {
|
||||||
// We use this to track the current scope
|
// We use this to track the current scope
|
||||||
pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
|
pub(crate) scope_stack: RefCell<Vec<ScopeId>>,
|
||||||
|
|
||||||
|
// We use this to track the current suspense location. Generally this lines up with the scope stack, but it may be different for children of a suspense boundary
|
||||||
|
pub(crate) suspense_stack: RefCell<Vec<SuspenseLocation>>,
|
||||||
|
|
||||||
// We use this to track the current task
|
// We use this to track the current task
|
||||||
pub(crate) current_task: Cell<Option<Task>>,
|
pub(crate) current_task: Cell<Option<Task>>,
|
||||||
|
|
||||||
|
@ -38,25 +40,26 @@ pub struct Runtime {
|
||||||
|
|
||||||
pub(crate) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
pub(crate) sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||||
|
|
||||||
// Synchronous tasks need to be run after the next render. The virtual dom stores a list of those tasks to send a signal to them when the next render is done.
|
|
||||||
pub(crate) render_signal: RenderSignal,
|
|
||||||
|
|
||||||
// The effects that need to be run after the next render
|
// The effects that need to be run after the next render
|
||||||
pub(crate) pending_effects: RefCell<BTreeSet<Effect>>,
|
pub(crate) pending_effects: RefCell<BTreeSet<Effect>>,
|
||||||
|
|
||||||
|
// Tasks that are waiting to be polled
|
||||||
|
pub(crate) dirty_tasks: RefCell<BTreeSet<DirtyTasks>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Runtime {
|
impl Runtime {
|
||||||
pub(crate) fn new(sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>) -> Rc<Self> {
|
pub(crate) fn new(sender: futures_channel::mpsc::UnboundedSender<SchedulerMsg>) -> Rc<Self> {
|
||||||
Rc::new(Self {
|
Rc::new(Self {
|
||||||
sender,
|
sender,
|
||||||
render_signal: RenderSignal::default(),
|
|
||||||
rendering: Cell::new(true),
|
rendering: Cell::new(true),
|
||||||
scope_states: Default::default(),
|
scope_states: Default::default(),
|
||||||
scope_stack: Default::default(),
|
scope_stack: Default::default(),
|
||||||
|
suspense_stack: Default::default(),
|
||||||
current_task: Default::default(),
|
current_task: Default::default(),
|
||||||
tasks: Default::default(),
|
tasks: Default::default(),
|
||||||
suspended_tasks: Default::default(),
|
suspended_tasks: Default::default(),
|
||||||
pending_effects: Default::default(),
|
pending_effects: Default::default(),
|
||||||
|
dirty_tasks: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,15 +114,34 @@ impl Runtime {
|
||||||
/// Useful in a limited number of scenarios
|
/// Useful in a limited number of scenarios
|
||||||
pub fn on_scope<O>(&self, id: ScopeId, f: impl FnOnce() -> O) -> O {
|
pub fn on_scope<O>(&self, id: ScopeId, f: impl FnOnce() -> O) -> O {
|
||||||
{
|
{
|
||||||
self.scope_stack.borrow_mut().push(id);
|
self.push_scope(id);
|
||||||
}
|
}
|
||||||
let o = f();
|
let o = f();
|
||||||
{
|
{
|
||||||
self.scope_stack.borrow_mut().pop();
|
self.pop_scope();
|
||||||
}
|
}
|
||||||
o
|
o
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push a scope onto the stack
|
||||||
|
pub(crate) fn push_scope(&self, scope: ScopeId) {
|
||||||
|
let suspense_location = self
|
||||||
|
.scope_states
|
||||||
|
.borrow()
|
||||||
|
.get(scope.0)
|
||||||
|
.and_then(|s| s.as_ref())
|
||||||
|
.map(|s| s.suspense_boundary())
|
||||||
|
.unwrap_or_default();
|
||||||
|
self.suspense_stack.borrow_mut().push(suspense_location);
|
||||||
|
self.scope_stack.borrow_mut().push(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pop a scope off the stack
|
||||||
|
pub(crate) fn pop_scope(&self) {
|
||||||
|
self.scope_stack.borrow_mut().pop();
|
||||||
|
self.suspense_stack.borrow_mut().pop();
|
||||||
|
}
|
||||||
|
|
||||||
/// Get the state for any scope given its ID
|
/// Get the state for any scope given its ID
|
||||||
///
|
///
|
||||||
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
/// This is useful for inserting or removing contexts from a scope, or rendering out its root node
|
||||||
|
@ -167,9 +189,18 @@ impl Runtime {
|
||||||
.unbounded_send(SchedulerMsg::EffectQueued)
|
.unbounded_send(SchedulerMsg::EffectQueued)
|
||||||
.expect("Scheduler should exist");
|
.expect("Scheduler should exist");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// And send the render signal
|
/// Check if we should render a scope
|
||||||
self.render_signal.send();
|
pub(crate) fn scope_should_render(&self, scope_id: ScopeId) -> bool {
|
||||||
|
// If there are no suspended futures, we know the scope is not and we can skip context checks
|
||||||
|
if self.suspended_tasks.get() == 0 {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If this is not a suspended scope, and we are under a frozen context, then we should
|
||||||
|
let scopes = self.scope_states.borrow();
|
||||||
|
let scope = &scopes[scope_id.0].as_ref().unwrap();
|
||||||
|
!matches!(scope.suspense_boundary(), SuspenseLocation::UnderSuspense(suspense) if suspense.suspended())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -124,12 +124,13 @@ impl Hash for ScopeOrder {
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
/// Queue a task to be polled
|
/// Queue a task to be polled
|
||||||
pub(crate) fn queue_task(&mut self, task: Task, order: ScopeOrder) {
|
pub(crate) fn queue_task(&mut self, task: Task, order: ScopeOrder) {
|
||||||
match self.dirty_tasks.get(&order) {
|
let mut dirty_tasks = self.runtime.dirty_tasks.borrow_mut();
|
||||||
|
match dirty_tasks.get(&order) {
|
||||||
Some(scope) => scope.queue_task(task),
|
Some(scope) => scope.queue_task(task),
|
||||||
None => {
|
None => {
|
||||||
let scope = DirtyTasks::from(order);
|
let scope = DirtyTasks::from(order);
|
||||||
scope.queue_task(task);
|
scope.queue_task(task);
|
||||||
self.dirty_tasks.insert(scope);
|
dirty_tasks.insert(scope);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -144,15 +145,23 @@ impl VirtualDom {
|
||||||
!self.dirty_scopes.is_empty()
|
!self.dirty_scopes.is_empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take any tasks from the highest scope
|
/// Take the top task from the highest scope
|
||||||
pub(crate) fn pop_task(&mut self) -> Option<DirtyTasks> {
|
pub(crate) fn pop_task(&mut self) -> Option<Task> {
|
||||||
let mut task = self.dirty_tasks.pop_first()?;
|
let mut dirty_tasks = self.runtime.dirty_tasks.borrow_mut();
|
||||||
|
let mut tasks = dirty_tasks.first()?;
|
||||||
|
|
||||||
// If the scope doesn't exist for whatever reason, then we should skip it
|
// If the scope doesn't exist for whatever reason, then we should skip it
|
||||||
while !self.scopes.contains(task.order.id.0) {
|
while !self.scopes.contains(tasks.order.id.0) {
|
||||||
task = self.dirty_tasks.pop_first()?;
|
dirty_tasks.pop_first();
|
||||||
|
tasks = dirty_tasks.first()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut tasks = tasks.tasks_queued.borrow_mut();
|
||||||
|
let task = tasks.pop_front()?;
|
||||||
|
if tasks.is_empty() {
|
||||||
|
drop(tasks);
|
||||||
|
dirty_tasks.pop_first();
|
||||||
|
}
|
||||||
Some(task)
|
Some(task)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,16 +191,21 @@ impl VirtualDom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut dirty_task = self.dirty_tasks.first();
|
// Find the height of the highest dirty scope
|
||||||
// Pop any invalid tasks off of each dirty scope;
|
let dirty_task = {
|
||||||
while let Some(task) = dirty_task {
|
let mut dirty_tasks = self.runtime.dirty_tasks.borrow_mut();
|
||||||
if !self.scopes.contains(task.order.id.0) {
|
let mut dirty_task = dirty_tasks.first();
|
||||||
self.dirty_tasks.pop_first();
|
// Pop any invalid tasks off of each dirty scope;
|
||||||
dirty_task = self.dirty_tasks.first();
|
while let Some(task) = dirty_task {
|
||||||
} else {
|
if task.tasks_queued.borrow().is_empty() || !self.scopes.contains(task.order.id.0) {
|
||||||
break;
|
dirty_tasks.pop_first();
|
||||||
|
dirty_task = dirty_tasks.first()
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
dirty_task.map(|task| task.order)
|
||||||
|
};
|
||||||
|
|
||||||
match (dirty_scope, dirty_task) {
|
match (dirty_scope, dirty_task) {
|
||||||
(Some(scope), Some(task)) => {
|
(Some(scope), Some(task)) => {
|
||||||
|
@ -199,57 +213,27 @@ impl VirtualDom {
|
||||||
match scope.cmp(tasks_order) {
|
match scope.cmp(tasks_order) {
|
||||||
std::cmp::Ordering::Less => {
|
std::cmp::Ordering::Less => {
|
||||||
let scope = self.dirty_scopes.pop_first().unwrap();
|
let scope = self.dirty_scopes.pop_first().unwrap();
|
||||||
Some(Work {
|
Some(Work::RerunScope(scope))
|
||||||
scope,
|
|
||||||
rerun_scope: true,
|
|
||||||
tasks: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
std::cmp::Ordering::Greater => {
|
std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => {
|
||||||
let task = self.dirty_tasks.pop_first().unwrap();
|
Some(Work::PollTask(self.pop_task().unwrap()))
|
||||||
Some(Work {
|
|
||||||
scope: task.order,
|
|
||||||
rerun_scope: false,
|
|
||||||
tasks: task.tasks_queued.into_inner(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
std::cmp::Ordering::Equal => {
|
|
||||||
let scope = self.dirty_scopes.pop_first().unwrap();
|
|
||||||
let task = self.dirty_tasks.pop_first().unwrap();
|
|
||||||
Some(Work {
|
|
||||||
scope,
|
|
||||||
rerun_scope: true,
|
|
||||||
tasks: task.tasks_queued.into_inner(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Some(_), None) => {
|
(Some(_), None) => {
|
||||||
let scope = self.dirty_scopes.pop_first().unwrap();
|
let scope = self.dirty_scopes.pop_first().unwrap();
|
||||||
Some(Work {
|
Some(Work::RerunScope(scope))
|
||||||
scope,
|
|
||||||
rerun_scope: true,
|
|
||||||
tasks: Default::default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
(None, Some(_)) => {
|
|
||||||
let task = self.dirty_tasks.pop_first().unwrap();
|
|
||||||
Some(Work {
|
|
||||||
scope: task.order,
|
|
||||||
rerun_scope: false,
|
|
||||||
tasks: task.tasks_queued.into_inner(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
(None, Some(_)) => Some(Work::PollTask(self.pop_task().unwrap())),
|
||||||
(None, None) => None,
|
(None, None) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Work {
|
pub enum Work {
|
||||||
pub scope: ScopeOrder,
|
RerunScope(ScopeOrder),
|
||||||
pub rerun_scope: bool,
|
PollTask(Task),
|
||||||
pub tasks: VecDeque<Task>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Eq)]
|
#[derive(Debug, Clone, Eq)]
|
||||||
|
@ -269,7 +253,16 @@ impl From<ScopeOrder> for DirtyTasks {
|
||||||
|
|
||||||
impl DirtyTasks {
|
impl DirtyTasks {
|
||||||
pub fn queue_task(&self, task: Task) {
|
pub fn queue_task(&self, task: Task) {
|
||||||
self.tasks_queued.borrow_mut().push_back(task);
|
let mut borrow_mut = self.tasks_queued.borrow_mut();
|
||||||
|
// If the task is already queued, we don't need to do anything
|
||||||
|
if borrow_mut.contains(&task) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
borrow_mut.push_back(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove(&self, id: Task) {
|
||||||
|
self.tasks_queued.borrow_mut().retain(|task| *task != id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,26 +1,38 @@
|
||||||
use crate::innerlude::ScopeOrder;
|
use crate::innerlude::{throw_error, RenderError, RenderReturn, ScopeOrder};
|
||||||
use crate::reactive_context::ReactiveContext;
|
use crate::prelude::ReactiveContext;
|
||||||
|
use crate::scope_context::SuspenseLocation;
|
||||||
use crate::{
|
use crate::{
|
||||||
any_props::{AnyProps, BoxedAnyProps},
|
any_props::{AnyProps, BoxedAnyProps},
|
||||||
innerlude::ScopeState,
|
innerlude::ScopeState,
|
||||||
nodes::RenderReturn,
|
|
||||||
scope_context::Scope,
|
scope_context::Scope,
|
||||||
scopes::ScopeId,
|
scopes::ScopeId,
|
||||||
virtual_dom::VirtualDom,
|
virtual_dom::VirtualDom,
|
||||||
};
|
};
|
||||||
|
use crate::{Element, VNode};
|
||||||
|
|
||||||
impl VirtualDom {
|
impl VirtualDom {
|
||||||
pub(super) fn new_scope(&mut self, props: BoxedAnyProps, name: &'static str) -> &ScopeState {
|
pub(super) fn new_scope(
|
||||||
|
&mut self,
|
||||||
|
props: BoxedAnyProps,
|
||||||
|
name: &'static str,
|
||||||
|
) -> &mut ScopeState {
|
||||||
let parent_id = self.runtime.current_scope_id();
|
let parent_id = self.runtime.current_scope_id();
|
||||||
let height = parent_id
|
let height = match parent_id.and_then(|id| self.runtime.get_state(id)) {
|
||||||
.and_then(|parent_id| self.runtime.get_state(parent_id).map(|f| f.height + 1))
|
Some(parent) => parent.height() + 1,
|
||||||
.unwrap_or(0);
|
None => 0,
|
||||||
|
};
|
||||||
|
let suspense_boundary = self
|
||||||
|
.runtime
|
||||||
|
.suspense_stack
|
||||||
|
.borrow()
|
||||||
|
.last()
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or(SuspenseLocation::NotSuspended);
|
||||||
let entry = self.scopes.vacant_entry();
|
let entry = self.scopes.vacant_entry();
|
||||||
let id = ScopeId(entry.key());
|
let id = ScopeId(entry.key());
|
||||||
|
|
||||||
let scope_runtime = Scope::new(name, id, parent_id, height);
|
let scope_runtime = Scope::new(name, id, parent_id, height, suspense_boundary);
|
||||||
let reactive_context = ReactiveContext::new_for_scope(&scope_runtime, &self.runtime);
|
let reactive_context = ReactiveContext::new_for_scope(&scope_runtime, &self.runtime);
|
||||||
self.runtime.create_scope(scope_runtime);
|
|
||||||
|
|
||||||
let scope = entry.insert(ScopeState {
|
let scope = entry.insert(ScopeState {
|
||||||
runtime: self.runtime.clone(),
|
runtime: self.runtime.clone(),
|
||||||
|
@ -30,60 +42,104 @@ impl VirtualDom {
|
||||||
reactive_context,
|
reactive_context,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
self.runtime.create_scope(scope_runtime);
|
||||||
|
tracing::trace!("created scope {id:?} with parent {parent_id:?}");
|
||||||
|
|
||||||
scope
|
scope
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Run a scope and return the rendered nodes. This will not modify the DOM or update the last rendered node of the scope.
|
||||||
|
#[tracing::instrument(skip(self), level = "trace", name = "VirtualDom::run_scope")]
|
||||||
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn {
|
pub(crate) fn run_scope(&mut self, scope_id: ScopeId) -> RenderReturn {
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
crate::Runtime::current().is_some(),
|
crate::Runtime::current().is_some(),
|
||||||
"Must be in a dioxus runtime"
|
"Must be in a dioxus runtime"
|
||||||
);
|
);
|
||||||
|
self.runtime.push_scope(scope_id);
|
||||||
|
|
||||||
self.runtime.scope_stack.borrow_mut().push(scope_id);
|
|
||||||
let scope = &self.scopes[scope_id.0];
|
let scope = &self.scopes[scope_id.0];
|
||||||
let new_nodes = {
|
let output = {
|
||||||
let context = scope.state();
|
let scope_state = scope.state();
|
||||||
|
|
||||||
context.hook_index.set(0);
|
scope_state.hook_index.set(0);
|
||||||
|
|
||||||
// Run all pre-render hooks
|
// Run all pre-render hooks
|
||||||
for pre_run in context.before_render.borrow_mut().iter_mut() {
|
for pre_run in scope_state.before_render.borrow_mut().iter_mut() {
|
||||||
pre_run();
|
pre_run();
|
||||||
}
|
}
|
||||||
|
|
||||||
// safety: due to how we traverse the tree, we know that the scope is not currently aliased
|
|
||||||
let props: &dyn AnyProps = &*scope.props;
|
let props: &dyn AnyProps = &*scope.props;
|
||||||
|
|
||||||
let span = tracing::trace_span!("render", scope = %scope.state().name);
|
let span = tracing::trace_span!("render", scope = %scope.state().name);
|
||||||
span.in_scope(|| scope.reactive_context.reset_and_run_in(|| props.render()))
|
span.in_scope(|| {
|
||||||
|
scope.reactive_context.reset_and_run_in(|| {
|
||||||
|
let mut render_return = props.render();
|
||||||
|
self.handle_element_return(&mut render_return.node, scope_id, &scope.state());
|
||||||
|
render_return
|
||||||
|
})
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let context = scope.state();
|
let scope_state = scope.state();
|
||||||
|
|
||||||
// Run all post-render hooks
|
// Run all post-render hooks
|
||||||
for post_run in context.after_render.borrow_mut().iter_mut() {
|
for post_run in scope_state.after_render.borrow_mut().iter_mut() {
|
||||||
post_run();
|
post_run();
|
||||||
}
|
}
|
||||||
|
|
||||||
// And move the render generation forward by one
|
|
||||||
context.render_count.set(context.render_count.get() + 1);
|
|
||||||
|
|
||||||
// remove this scope from dirty scopes
|
// remove this scope from dirty scopes
|
||||||
self.dirty_scopes
|
self.dirty_scopes
|
||||||
.remove(&ScopeOrder::new(context.height, scope_id));
|
.remove(&ScopeOrder::new(scope_state.height, scope_id));
|
||||||
|
|
||||||
if let Some(task) = context.last_suspendable_task.take() {
|
self.runtime.pop_scope();
|
||||||
if matches!(new_nodes, RenderReturn::Aborted(_)) {
|
|
||||||
tracing::trace!("Suspending {:?} on {:?}", scope_id, task);
|
output
|
||||||
self.runtime.tasks.borrow().get(task.0).unwrap().suspend();
|
}
|
||||||
self.runtime
|
|
||||||
.suspended_tasks
|
/// Insert any errors, or suspended tasks from an element return into the runtime
|
||||||
.set(self.runtime.suspended_tasks.get() + 1);
|
fn handle_element_return(&self, node: &mut Element, scope_id: ScopeId, scope_state: &Scope) {
|
||||||
|
match node {
|
||||||
|
Err(RenderError::Aborted(e)) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Error while rendering component `{}`: {e:?}",
|
||||||
|
scope_state.name
|
||||||
|
);
|
||||||
|
throw_error(e.clone_mounted());
|
||||||
|
e.render = VNode::placeholder();
|
||||||
|
}
|
||||||
|
Err(RenderError::Suspended(e)) => {
|
||||||
|
let task = e.task();
|
||||||
|
// Insert the task into the nearest suspense boundary if it exists
|
||||||
|
let boundary = self
|
||||||
|
.runtime
|
||||||
|
.get_state(scope_id)
|
||||||
|
.unwrap()
|
||||||
|
.suspense_boundary();
|
||||||
|
let already_suspended = self
|
||||||
|
.runtime
|
||||||
|
.tasks
|
||||||
|
.borrow()
|
||||||
|
.get(task.id)
|
||||||
|
.expect("Suspended on a task that no longer exists")
|
||||||
|
.suspend(boundary.clone());
|
||||||
|
if !already_suspended {
|
||||||
|
tracing::trace!("Suspending {:?} on {:?}", scope_id, task);
|
||||||
|
// Add this task to the suspended tasks list of the boundary
|
||||||
|
if let SuspenseLocation::UnderSuspense(boundary) = &boundary {
|
||||||
|
boundary.add_suspended_task(e.clone());
|
||||||
|
}
|
||||||
|
self.runtime
|
||||||
|
.suspended_tasks
|
||||||
|
.set(self.runtime.suspended_tasks.get() + 1);
|
||||||
|
}
|
||||||
|
e.placeholder = VNode::placeholder();
|
||||||
|
}
|
||||||
|
Ok(_) => {
|
||||||
|
// If the render was successful, we can move the render generation forward by one
|
||||||
|
scope_state
|
||||||
|
.render_count
|
||||||
|
.set(scope_state.render_count.get() + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.runtime.scope_stack.borrow_mut().pop();
|
|
||||||
|
|
||||||
new_nodes
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,8 @@
|
||||||
use crate::{innerlude::SchedulerMsg, Element, Runtime, ScopeId, Task};
|
use crate::{innerlude::SchedulerMsg, Runtime, ScopeId, Task};
|
||||||
|
use crate::{
|
||||||
|
innerlude::{throw_into, CapturedError},
|
||||||
|
prelude::SuspenseContext,
|
||||||
|
};
|
||||||
use generational_box::{AnyStorage, Owner};
|
use generational_box::{AnyStorage, Owner};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -8,6 +12,32 @@ use std::{
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub(crate) enum ScopeStatus {
|
||||||
|
Mounted,
|
||||||
|
Unmounted {
|
||||||
|
// Before the component is mounted, we need to keep track of effects that need to be run once the scope is mounted
|
||||||
|
effects_queued: Vec<Box<dyn FnOnce() + 'static>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub(crate) enum SuspenseLocation {
|
||||||
|
#[default]
|
||||||
|
NotSuspended,
|
||||||
|
InSuspensePlaceholder(SuspenseContext),
|
||||||
|
UnderSuspense(SuspenseContext),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuspenseLocation {
|
||||||
|
pub(crate) fn suspense_context(&self) -> Option<&SuspenseContext> {
|
||||||
|
match self {
|
||||||
|
SuspenseLocation::InSuspensePlaceholder(context) => Some(context),
|
||||||
|
SuspenseLocation::UnderSuspense(context) => Some(context),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A component's state separate from its props.
|
/// A component's state separate from its props.
|
||||||
///
|
///
|
||||||
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
/// This struct exists to provide a common interface for all scopes without relying on generics.
|
||||||
|
@ -23,10 +53,13 @@ pub(crate) struct Scope {
|
||||||
pub(crate) hook_index: Cell<usize>,
|
pub(crate) hook_index: Cell<usize>,
|
||||||
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
|
pub(crate) shared_contexts: RefCell<Vec<Box<dyn Any>>>,
|
||||||
pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
|
pub(crate) spawned_tasks: RefCell<FxHashSet<Task>>,
|
||||||
/// The task that was last spawned that may suspend. We use this task to check what task to suspend in the event of an early None return from a component
|
|
||||||
pub(crate) last_suspendable_task: Cell<Option<Task>>,
|
|
||||||
pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
|
pub(crate) before_render: RefCell<Vec<Box<dyn FnMut()>>>,
|
||||||
pub(crate) after_render: RefCell<Vec<Box<dyn FnMut()>>>,
|
pub(crate) after_render: RefCell<Vec<Box<dyn FnMut()>>>,
|
||||||
|
|
||||||
|
/// The suspense boundary that this scope is currently in (if any)
|
||||||
|
suspense_boundary: SuspenseLocation,
|
||||||
|
|
||||||
|
pub(crate) status: RefCell<ScopeStatus>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scope {
|
impl Scope {
|
||||||
|
@ -35,6 +68,7 @@ impl Scope {
|
||||||
id: ScopeId,
|
id: ScopeId,
|
||||||
parent_id: Option<ScopeId>,
|
parent_id: Option<ScopeId>,
|
||||||
height: u32,
|
height: u32,
|
||||||
|
suspense_boundary: SuspenseLocation,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name,
|
name,
|
||||||
|
@ -44,11 +78,14 @@ impl Scope {
|
||||||
render_count: Cell::new(0),
|
render_count: Cell::new(0),
|
||||||
shared_contexts: RefCell::new(vec![]),
|
shared_contexts: RefCell::new(vec![]),
|
||||||
spawned_tasks: RefCell::new(FxHashSet::default()),
|
spawned_tasks: RefCell::new(FxHashSet::default()),
|
||||||
last_suspendable_task: Cell::new(None),
|
|
||||||
hooks: RefCell::new(vec![]),
|
hooks: RefCell::new(vec![]),
|
||||||
hook_index: Cell::new(0),
|
hook_index: Cell::new(0),
|
||||||
before_render: RefCell::new(vec![]),
|
before_render: RefCell::new(vec![]),
|
||||||
after_render: RefCell::new(vec![]),
|
after_render: RefCell::new(vec![]),
|
||||||
|
status: RefCell::new(ScopeStatus::Unmounted {
|
||||||
|
effects_queued: Vec::new(),
|
||||||
|
}),
|
||||||
|
suspense_boundary,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +97,30 @@ impl Scope {
|
||||||
Runtime::with(|rt| rt.sender.clone()).unwrap()
|
Runtime::with(|rt| rt.sender.clone()).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Mount the scope and queue any pending effects if it is not already mounted
|
||||||
|
pub(crate) fn mount(&self, runtime: &Runtime) {
|
||||||
|
let mut status = self.status.borrow_mut();
|
||||||
|
if let ScopeStatus::Unmounted { effects_queued } = &mut *status {
|
||||||
|
for f in effects_queued.drain(..) {
|
||||||
|
runtime.queue_effect_on_mounted_scope(self.id, f);
|
||||||
|
}
|
||||||
|
*status = ScopeStatus::Mounted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the suspense boundary this scope is currently in (if any)
|
||||||
|
pub(crate) fn suspense_boundary(&self) -> SuspenseLocation {
|
||||||
|
self.suspense_boundary.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if a node should run during suspense
|
||||||
|
pub(crate) fn should_run_during_suspense(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.suspense_boundary,
|
||||||
|
SuspenseLocation::UnderSuspense(_) | SuspenseLocation::InSuspensePlaceholder(_)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Mark this scope as dirty, and schedule a render for it.
|
/// Mark this scope as dirty, and schedule a render for it.
|
||||||
pub fn needs_update(&self) {
|
pub fn needs_update(&self) {
|
||||||
self.needs_update_any(self.id)
|
self.needs_update_any(self.id)
|
||||||
|
@ -129,18 +190,18 @@ impl Scope {
|
||||||
let mut search_parent = self.parent_id;
|
let mut search_parent = self.parent_id;
|
||||||
let cur_runtime = Runtime::with(|runtime| {
|
let cur_runtime = Runtime::with(|runtime| {
|
||||||
while let Some(parent_id) = search_parent {
|
while let Some(parent_id) = search_parent {
|
||||||
let parent = runtime.get_state(parent_id).unwrap();
|
let Some(parent) = runtime.get_state(parent_id) else {
|
||||||
|
tracing::error!("Parent scope {:?} not found", parent_id);
|
||||||
|
return None;
|
||||||
|
};
|
||||||
tracing::trace!(
|
tracing::trace!(
|
||||||
"looking for context {} ({:?}) in {}",
|
"looking for context {} ({:?}) in {}",
|
||||||
std::any::type_name::<T>(),
|
std::any::type_name::<T>(),
|
||||||
std::any::TypeId::of::<T>(),
|
std::any::TypeId::of::<T>(),
|
||||||
parent.name
|
parent.name
|
||||||
);
|
);
|
||||||
if let Some(shared) = parent.shared_contexts.borrow().iter().find_map(|any| {
|
if let Some(shared) = parent.has_context() {
|
||||||
tracing::trace!("found context {:?}", (**any).type_id());
|
return Some(shared);
|
||||||
any.downcast_ref::<T>()
|
|
||||||
}) {
|
|
||||||
return Some(shared.clone());
|
|
||||||
}
|
}
|
||||||
search_parent = parent.parent_id;
|
search_parent = parent.parent_id;
|
||||||
}
|
}
|
||||||
|
@ -286,12 +347,6 @@ impl Scope {
|
||||||
Runtime::with(|rt| rt.queue_effect(self.id, f)).expect("Runtime to exist");
|
Runtime::with(|rt| rt.queue_effect(self.id, f)).expect("Runtime to exist");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mark this component as suspended on a specific task and then return None
|
|
||||||
pub fn suspend(&self, task: Task) -> Option<Element> {
|
|
||||||
self.last_suspendable_task.set(Some(task));
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Store a value between renders. The foundational hook for all other hooks.
|
/// Store a value between renders. The foundational hook for all other hooks.
|
||||||
///
|
///
|
||||||
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render).
|
/// Accepts an `initializer` closure, which is run on the first use of the hook (typically the initial render).
|
||||||
|
@ -413,14 +468,6 @@ impl ScopeId {
|
||||||
.expect("to be in a dioxus runtime")
|
.expect("to be in a dioxus runtime")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Suspended a component on a specific task and then return None
|
|
||||||
pub fn suspend(self, task: Task) -> Option<Element> {
|
|
||||||
Runtime::with_scope(self, |cx| {
|
|
||||||
cx.suspend(task);
|
|
||||||
});
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pushes the future onto the poll queue to be polled after the component renders.
|
/// Pushes the future onto the poll queue to be polled after the component renders.
|
||||||
pub fn push_future(self, fut: impl Future<Output = ()> + 'static) -> Option<Task> {
|
pub fn push_future(self, fut: impl Future<Output = ()> + 'static) -> Option<Task> {
|
||||||
Runtime::with_scope(self, |cx| cx.spawn(fut))
|
Runtime::with_scope(self, |cx| cx.spawn(fut))
|
||||||
|
@ -466,4 +513,25 @@ impl ScopeId {
|
||||||
.expect("to be in a dioxus runtime")
|
.expect("to be in a dioxus runtime")
|
||||||
.on_scope(self, f)
|
.on_scope(self, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Throw a [`CapturedError`] into a scope. The error will bubble up to the nearest [`ErrorBoundary`] or the root of the app.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// fn Component() -> Element {
|
||||||
|
/// let request = spawn(async move {
|
||||||
|
/// match reqwest::get("https://api.example.com").await {
|
||||||
|
/// Ok(_) => todo!(),
|
||||||
|
/// // You can explicitly throw an error into a scope with throw_error
|
||||||
|
/// Err(err) => ScopeId::APP.throw_error(err)
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
///
|
||||||
|
/// todo!()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
pub fn throw_error(self, error: impl Into<CapturedError> + 'static) {
|
||||||
|
throw_into(error, self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
any_props::BoxedAnyProps, nodes::RenderReturn, reactive_context::ReactiveContext,
|
any_props::BoxedAnyProps, nodes::RenderReturn, reactive_context::ReactiveContext,
|
||||||
runtime::Runtime, scope_context::Scope,
|
scope_context::Scope, Runtime, VNode,
|
||||||
};
|
};
|
||||||
use std::{cell::Ref, rc::Rc};
|
use std::{cell::Ref, rc::Rc};
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ impl std::fmt::Debug for ScopeId {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScopeId {
|
impl ScopeId {
|
||||||
/// The root ScopeId.
|
/// The ScopeId of the main scope passed into [`VirtualDom::new`].
|
||||||
///
|
///
|
||||||
/// This scope will last for the entire duration of your app, making it convenient for long-lived state
|
/// This scope will last for the entire duration of your app, making it convenient for long-lived state
|
||||||
/// that is created dynamically somewhere down the component tree.
|
/// that is created dynamically somewhere down the component tree.
|
||||||
|
@ -41,9 +41,24 @@ impl ScopeId {
|
||||||
///
|
///
|
||||||
/// ```rust, no_run
|
/// ```rust, no_run
|
||||||
/// use dioxus::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
/// let my_persistent_state = Signal::new_in_scope(String::new(), ScopeId::ROOT);
|
/// let my_persistent_state = Signal::new_in_scope(String::new(), ScopeId::APP);
|
||||||
/// ```
|
/// ```
|
||||||
|
// ScopeId(0) is the root scope wrapper
|
||||||
|
// ScopeId(1) is the default error boundary
|
||||||
|
// ScopeId(2) is the default suspense boundary
|
||||||
|
// ScopeId(3) is the users root scope
|
||||||
|
pub const APP: ScopeId = ScopeId(3);
|
||||||
|
|
||||||
|
/// The ScopeId of the topmost scope in the tree.
|
||||||
|
/// This will be higher up in the tree than [`ScopeId::APP`] because dioxus inserts a default [`SuspenseBoundary`] and [`ErrorBoundary`] at the root of the tree.
|
||||||
|
// ScopeId(0) is the root scope wrapper
|
||||||
pub const ROOT: ScopeId = ScopeId(0);
|
pub const ROOT: ScopeId = ScopeId(0);
|
||||||
|
|
||||||
|
pub(crate) const PLACEHOLDER: ScopeId = ScopeId(usize::MAX);
|
||||||
|
|
||||||
|
pub(crate) fn is_placeholder(&self) -> bool {
|
||||||
|
*self == Self::PLACEHOLDER
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A component's rendered state.
|
/// A component's rendered state.
|
||||||
|
@ -52,6 +67,8 @@ impl ScopeId {
|
||||||
pub struct ScopeState {
|
pub struct ScopeState {
|
||||||
pub(crate) runtime: Rc<Runtime>,
|
pub(crate) runtime: Rc<Runtime>,
|
||||||
pub(crate) context_id: ScopeId,
|
pub(crate) context_id: ScopeId,
|
||||||
|
/// The last node that has been rendered for this component. This node may not ben mounted
|
||||||
|
/// During suspense, this component can be rendered in the background multiple times
|
||||||
pub(crate) last_rendered_node: Option<RenderReturn>,
|
pub(crate) last_rendered_node: Option<RenderReturn>,
|
||||||
pub(crate) props: BoxedAnyProps,
|
pub(crate) props: BoxedAnyProps,
|
||||||
pub(crate) reactive_context: ReactiveContext,
|
pub(crate) reactive_context: ReactiveContext,
|
||||||
|
@ -69,7 +86,7 @@ impl ScopeState {
|
||||||
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
|
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
|
||||||
///
|
///
|
||||||
/// Panics if the tree has not been built yet.
|
/// Panics if the tree has not been built yet.
|
||||||
pub fn root_node(&self) -> &RenderReturn {
|
pub fn root_node(&self) -> &VNode {
|
||||||
self.try_root_node()
|
self.try_root_node()
|
||||||
.expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.")
|
.expect("The tree has not been built yet. Make sure to call rebuild on the tree before accessing its nodes.")
|
||||||
}
|
}
|
||||||
|
@ -79,8 +96,13 @@ impl ScopeState {
|
||||||
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
|
/// This is useful for traversing the tree outside of the VirtualDom, such as in a custom renderer or in SSR.
|
||||||
///
|
///
|
||||||
/// Returns [`None`] if the tree has not been built yet.
|
/// Returns [`None`] if the tree has not been built yet.
|
||||||
pub fn try_root_node(&self) -> Option<&RenderReturn> {
|
pub fn try_root_node(&self) -> Option<&VNode> {
|
||||||
self.last_rendered_node.as_ref()
|
self.last_rendered_node.as_deref()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the scope id of this [`ScopeState`].
|
||||||
|
pub fn id(&self) -> ScopeId {
|
||||||
|
self.context_id
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn state(&self) -> Ref<'_, Scope> {
|
pub(crate) fn state(&self) -> Ref<'_, Scope> {
|
||||||
|
|
607
packages/core/src/suspense/component.rs
Normal file
607
packages/core/src/suspense/component.rs
Normal file
|
@ -0,0 +1,607 @@
|
||||||
|
use crate::innerlude::*;
|
||||||
|
|
||||||
|
/// Properties for the [`SuspenseBoundary()`] component.
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub struct SuspenseBoundaryProps {
|
||||||
|
fallback: Callback<SuspenseContext, Element>,
|
||||||
|
/// The children of the suspense boundary
|
||||||
|
children: Element,
|
||||||
|
/// THe nodes that are suspended under this boundary
|
||||||
|
pub suspended_nodes: Option<VNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for SuspenseBoundaryProps {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
fallback: self.fallback,
|
||||||
|
children: self.children.clone(),
|
||||||
|
suspended_nodes: self
|
||||||
|
.suspended_nodes
|
||||||
|
.as_ref()
|
||||||
|
.map(|node| node.clone_mounted()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuspenseBoundaryProps {
|
||||||
|
/**
|
||||||
|
Create a builder for building `SuspenseBoundaryProps`.
|
||||||
|
On the builder, call `.fallback(...)`, `.children(...)`(optional) to set the values of the fields.
|
||||||
|
Finally, call `.build()` to create the instance of `SuspenseBoundaryProps`.
|
||||||
|
*/
|
||||||
|
#[allow(dead_code, clippy::type_complexity)]
|
||||||
|
fn builder() -> SuspenseBoundaryPropsBuilder<((), ())> {
|
||||||
|
SuspenseBoundaryPropsBuilder {
|
||||||
|
owner: Owner::default(),
|
||||||
|
fields: ((), ()),
|
||||||
|
_phantom: ::core::default::Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[must_use]
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub struct SuspenseBoundaryPropsBuilder<TypedBuilderFields> {
|
||||||
|
owner: Owner,
|
||||||
|
fields: TypedBuilderFields,
|
||||||
|
_phantom: (),
|
||||||
|
}
|
||||||
|
impl Properties for SuspenseBoundaryProps
|
||||||
|
where
|
||||||
|
Self: Clone,
|
||||||
|
{
|
||||||
|
type Builder = SuspenseBoundaryPropsBuilder<((), ())>;
|
||||||
|
fn builder() -> Self::Builder {
|
||||||
|
SuspenseBoundaryProps::builder()
|
||||||
|
}
|
||||||
|
fn memoize(&mut self, new: &Self) -> bool {
|
||||||
|
let equal = self == new;
|
||||||
|
self.fallback.__set(new.fallback.__take());
|
||||||
|
if !equal {
|
||||||
|
let new_clone = new.clone();
|
||||||
|
self.children = new_clone.children;
|
||||||
|
}
|
||||||
|
equal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub trait SuspenseBoundaryPropsBuilder_Optional<T> {
|
||||||
|
fn into_value<F: FnOnce() -> T>(self, default: F) -> T;
|
||||||
|
}
|
||||||
|
impl<T> SuspenseBoundaryPropsBuilder_Optional<T> for () {
|
||||||
|
fn into_value<F: FnOnce() -> T>(self, default: F) -> T {
|
||||||
|
default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<T> SuspenseBoundaryPropsBuilder_Optional<T> for (T,) {
|
||||||
|
fn into_value<F: FnOnce() -> T>(self, _: F) -> T {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__children> SuspenseBoundaryPropsBuilder<((), __children)> {
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn fallback<__Marker>(
|
||||||
|
self,
|
||||||
|
fallback: impl SuperInto<Callback<SuspenseContext, Element>, __Marker>,
|
||||||
|
) -> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
|
||||||
|
let fallback = (with_owner(self.owner.clone(), move || {
|
||||||
|
SuperInto::super_into(fallback)
|
||||||
|
}),);
|
||||||
|
let (_, children) = self.fields;
|
||||||
|
SuspenseBoundaryPropsBuilder {
|
||||||
|
owner: self.owner,
|
||||||
|
fields: (fallback, children),
|
||||||
|
_phantom: self._phantom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub enum SuspenseBoundaryPropsBuilder_Error_Repeated_field_fallback {}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__children> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
|
||||||
|
#[deprecated(note = "Repeated field fallback")]
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn fallback(
|
||||||
|
self,
|
||||||
|
_: SuspenseBoundaryPropsBuilder_Error_Repeated_field_fallback,
|
||||||
|
) -> SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__fallback> SuspenseBoundaryPropsBuilder<(__fallback, ())> {
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn children(
|
||||||
|
self,
|
||||||
|
children: Element,
|
||||||
|
) -> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
|
||||||
|
let children = (children,);
|
||||||
|
let (fallback, _) = self.fields;
|
||||||
|
SuspenseBoundaryPropsBuilder {
|
||||||
|
owner: self.owner,
|
||||||
|
fields: (fallback, children),
|
||||||
|
_phantom: self._phantom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub enum SuspenseBoundaryPropsBuilder_Error_Repeated_field_children {}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__fallback> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
|
||||||
|
#[deprecated(note = "Repeated field children")]
|
||||||
|
#[allow(clippy::type_complexity)]
|
||||||
|
pub fn children(
|
||||||
|
self,
|
||||||
|
_: SuspenseBoundaryPropsBuilder_Error_Repeated_field_children,
|
||||||
|
) -> SuspenseBoundaryPropsBuilder<(__fallback, (Element,))> {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, non_snake_case)]
|
||||||
|
pub enum SuspenseBoundaryPropsBuilder_Error_Missing_required_field_fallback {}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs, clippy::panic)]
|
||||||
|
impl<__children> SuspenseBoundaryPropsBuilder<((), __children)> {
|
||||||
|
#[deprecated(note = "Missing required field fallback")]
|
||||||
|
pub fn build(
|
||||||
|
self,
|
||||||
|
_: SuspenseBoundaryPropsBuilder_Error_Missing_required_field_fallback,
|
||||||
|
) -> SuspenseBoundaryProps {
|
||||||
|
panic!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
pub struct SuspenseBoundaryPropsWithOwner {
|
||||||
|
inner: SuspenseBoundaryProps,
|
||||||
|
owner: Owner,
|
||||||
|
}
|
||||||
|
#[automatically_derived]
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl ::core::clone::Clone for SuspenseBoundaryPropsWithOwner {
|
||||||
|
#[inline]
|
||||||
|
fn clone(&self) -> SuspenseBoundaryPropsWithOwner {
|
||||||
|
SuspenseBoundaryPropsWithOwner {
|
||||||
|
inner: ::core::clone::Clone::clone(&self.inner),
|
||||||
|
owner: ::core::clone::Clone::clone(&self.owner),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl PartialEq for SuspenseBoundaryPropsWithOwner {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.inner.eq(&other.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl SuspenseBoundaryPropsWithOwner {
|
||||||
|
/// Create a component from the props.
|
||||||
|
pub fn into_vcomponent<M: 'static>(
|
||||||
|
self,
|
||||||
|
render_fn: impl ComponentFunction<SuspenseBoundaryProps, M>,
|
||||||
|
component_name: &'static str,
|
||||||
|
) -> VComponent {
|
||||||
|
VComponent::new(
|
||||||
|
move |wrapper: Self| render_fn.rebuild(wrapper.inner),
|
||||||
|
self,
|
||||||
|
component_name,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Properties for SuspenseBoundaryPropsWithOwner {
|
||||||
|
type Builder = ();
|
||||||
|
fn builder() -> Self::Builder {
|
||||||
|
unreachable!()
|
||||||
|
}
|
||||||
|
fn memoize(&mut self, new: &Self) -> bool {
|
||||||
|
self.inner.memoize(&new.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[allow(dead_code, non_camel_case_types, missing_docs)]
|
||||||
|
impl<__children: SuspenseBoundaryPropsBuilder_Optional<Element>>
|
||||||
|
SuspenseBoundaryPropsBuilder<((Callback<SuspenseContext, Element>,), __children)>
|
||||||
|
{
|
||||||
|
pub fn build(self) -> SuspenseBoundaryPropsWithOwner {
|
||||||
|
let (fallback, children) = self.fields;
|
||||||
|
let fallback = fallback.0;
|
||||||
|
let children = SuspenseBoundaryPropsBuilder_Optional::into_value(children, VNode::empty);
|
||||||
|
SuspenseBoundaryPropsWithOwner {
|
||||||
|
inner: SuspenseBoundaryProps {
|
||||||
|
fallback,
|
||||||
|
children,
|
||||||
|
suspended_nodes: None,
|
||||||
|
},
|
||||||
|
owner: self.owner,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[automatically_derived]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
impl ::core::cmp::PartialEq for SuspenseBoundaryProps {
|
||||||
|
#[inline]
|
||||||
|
fn eq(&self, other: &SuspenseBoundaryProps) -> bool {
|
||||||
|
self.fallback == other.fallback && self.children == other.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Suspense Boundaries let you render a fallback UI while a child component is suspended.
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// # fn Article() -> Element { rsx! { "Article" } }
|
||||||
|
/// fn App() -> Element {
|
||||||
|
/// rsx! {
|
||||||
|
/// SuspenseBoundary {
|
||||||
|
/// fallback: |context: SuspenseContext| rsx! {
|
||||||
|
/// if let Some(placeholder) = context.suspense_placeholder() {
|
||||||
|
/// {placeholder}
|
||||||
|
/// } else {
|
||||||
|
/// "Loading..."
|
||||||
|
/// }
|
||||||
|
/// },
|
||||||
|
/// Article {}
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
pub fn SuspenseBoundary(mut __props: SuspenseBoundaryProps) -> Element {
|
||||||
|
unreachable!("SuspenseBoundary should not be called directly")
|
||||||
|
}
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
mod SuspenseBoundary_completions {
|
||||||
|
#[doc(hidden)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
/// This enum is generated to help autocomplete the braces after the component. It does nothing
|
||||||
|
pub enum Component {
|
||||||
|
SuspenseBoundary {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
use generational_box::Owner;
|
||||||
|
#[allow(unused)]
|
||||||
|
pub use SuspenseBoundary_completions::Component::SuspenseBoundary;
|
||||||
|
|
||||||
|
/// Suspense has a custom diffing algorithm that diffs the suspended nodes in the background without rendering them
|
||||||
|
impl SuspenseBoundaryProps {
|
||||||
|
/// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`]
|
||||||
|
pub(crate) fn downcast_mut_from_props(props: &mut dyn AnyProps) -> Option<&mut Self> {
|
||||||
|
let inner: Option<&mut SuspenseBoundaryPropsWithOwner> = props.props_mut().downcast_mut();
|
||||||
|
inner.map(|inner| &mut inner.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to downcast [`AnyProps`] to [`SuspenseBoundaryProps`]
|
||||||
|
pub(crate) fn downcast_ref_from_props(props: &dyn AnyProps) -> Option<&Self> {
|
||||||
|
let inner: Option<&SuspenseBoundaryPropsWithOwner> = props.props().downcast_ref();
|
||||||
|
inner.map(|inner| &inner.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Try to extract [`SuspenseBoundaryProps`] from [`ScopeState`]
|
||||||
|
pub fn downcast_from_scope(scope_state: &ScopeState) -> Option<&Self> {
|
||||||
|
let inner: Option<&SuspenseBoundaryPropsWithOwner> =
|
||||||
|
scope_state.props.props().downcast_ref();
|
||||||
|
inner.map(|inner| &inner.inner)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the suspense boundary is currently holding its children in suspense
|
||||||
|
pub fn suspended(&self) -> bool {
|
||||||
|
self.suspended_nodes.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn create<M: WriteMutations>(
|
||||||
|
mount: MountId,
|
||||||
|
idx: usize,
|
||||||
|
component: &VComponent,
|
||||||
|
parent: Option<ElementRef>,
|
||||||
|
dom: &mut VirtualDom,
|
||||||
|
to: Option<&mut M>,
|
||||||
|
) -> usize {
|
||||||
|
let mut scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]);
|
||||||
|
|
||||||
|
// If the ScopeId is a placeholder, we need to load up a new scope for this vcomponent. If it's already mounted, then we can just use that
|
||||||
|
if scope_id.is_placeholder() {
|
||||||
|
{
|
||||||
|
let suspense_context = SuspenseContext::new();
|
||||||
|
|
||||||
|
dom.runtime.suspense_stack.borrow_mut().push(
|
||||||
|
crate::scope_context::SuspenseLocation::UnderSuspense(suspense_context.clone()),
|
||||||
|
);
|
||||||
|
{
|
||||||
|
let scope_state = dom
|
||||||
|
.new_scope(component.props.duplicate(), component.name)
|
||||||
|
.state();
|
||||||
|
suspense_context.mount(scope_state.id);
|
||||||
|
scope_id = scope_state.id;
|
||||||
|
}
|
||||||
|
dom.runtime.suspense_stack.borrow_mut().pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the scope id for the next render
|
||||||
|
dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let scope_state = &mut dom.scopes[scope_id.0];
|
||||||
|
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap();
|
||||||
|
|
||||||
|
let children = RenderReturn {
|
||||||
|
node: props
|
||||||
|
.children
|
||||||
|
.as_ref()
|
||||||
|
.map(|node| node.clone_mounted())
|
||||||
|
.map_err(Clone::clone),
|
||||||
|
};
|
||||||
|
|
||||||
|
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||||
|
dom.runtime.push_scope(scope_id);
|
||||||
|
children.create(dom, parent, None::<&mut M>);
|
||||||
|
dom.runtime.pop_scope();
|
||||||
|
|
||||||
|
// Store the (now mounted) children back into the scope state
|
||||||
|
let scope_state = &mut dom.scopes[scope_id.0];
|
||||||
|
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap();
|
||||||
|
props.children = children.clone().node;
|
||||||
|
|
||||||
|
let scope_state = &mut dom.scopes[scope_id.0];
|
||||||
|
let suspense_context = scope_state
|
||||||
|
.state()
|
||||||
|
.suspense_boundary()
|
||||||
|
.suspense_context()
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
// If there are suspended futures, render the fallback
|
||||||
|
let nodes_created = if !suspense_context.suspended_futures().is_empty() {
|
||||||
|
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap();
|
||||||
|
props.suspended_nodes = Some(children.into());
|
||||||
|
|
||||||
|
dom.runtime.suspense_stack.borrow_mut().push(
|
||||||
|
crate::scope_context::SuspenseLocation::InSuspensePlaceholder(
|
||||||
|
suspense_context.clone(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let suspense_placeholder = props.fallback.call(suspense_context);
|
||||||
|
let node = RenderReturn {
|
||||||
|
node: suspense_placeholder,
|
||||||
|
};
|
||||||
|
let nodes_created = node.create(dom, parent, to);
|
||||||
|
dom.runtime.suspense_stack.borrow_mut().pop();
|
||||||
|
|
||||||
|
let scope_state = &mut dom.scopes[scope_id.0];
|
||||||
|
scope_state.last_rendered_node = Some(node);
|
||||||
|
|
||||||
|
nodes_created
|
||||||
|
} else {
|
||||||
|
// Otherwise just render the children in the real dom
|
||||||
|
dom.runtime.push_scope(scope_id);
|
||||||
|
debug_assert!(children.mount.get().mounted());
|
||||||
|
let nodes_created = children.create(dom, parent, to);
|
||||||
|
dom.runtime.pop_scope();
|
||||||
|
let scope_state = &mut dom.scopes[scope_id.0];
|
||||||
|
scope_state.last_rendered_node = Some(children);
|
||||||
|
let props = Self::downcast_mut_from_props(&mut *dom.scopes[scope_id.0].props).unwrap();
|
||||||
|
props.suspended_nodes = None;
|
||||||
|
nodes_created
|
||||||
|
};
|
||||||
|
|
||||||
|
nodes_created
|
||||||
|
}
|
||||||
|
|
||||||
|
#[doc(hidden)]
|
||||||
|
/// Manually rerun the children of this suspense boundary without diffing against the old nodes.
|
||||||
|
///
|
||||||
|
/// This should only be called by dioxus-web after the suspense boundary has been streamed in from the server.
|
||||||
|
pub fn resolve_suspense<M: WriteMutations>(
|
||||||
|
scope_id: ScopeId,
|
||||||
|
dom: &mut VirtualDom,
|
||||||
|
to: &mut M,
|
||||||
|
only_write_templates: impl FnOnce(&mut M),
|
||||||
|
replace_with: usize,
|
||||||
|
) {
|
||||||
|
let _runtime = RuntimeGuard::new(dom.runtime.clone());
|
||||||
|
let Some(scope_state) = dom.scopes.get_mut(scope_id.0) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Reset the suspense context
|
||||||
|
let suspense_context = scope_state
|
||||||
|
.state()
|
||||||
|
.suspense_boundary()
|
||||||
|
.suspense_context()
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
suspense_context.inner.suspended_tasks.borrow_mut().clear();
|
||||||
|
|
||||||
|
// Get the parent of the suspense boundary to later create children with the right parent
|
||||||
|
let currently_rendered = scope_state.last_rendered_node.as_ref().unwrap().clone();
|
||||||
|
let mount = currently_rendered.mount.get();
|
||||||
|
let parent = dom
|
||||||
|
.mounts
|
||||||
|
.get(mount.0)
|
||||||
|
.expect("suspense placeholder is not mounted")
|
||||||
|
.parent;
|
||||||
|
|
||||||
|
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap();
|
||||||
|
|
||||||
|
// Unmount any children to reset any scopes under this suspense boundary
|
||||||
|
let children = props
|
||||||
|
.children
|
||||||
|
.as_ref()
|
||||||
|
.map(|node| node.clone_mounted())
|
||||||
|
.map_err(Clone::clone);
|
||||||
|
let suspended = props
|
||||||
|
.suspended_nodes
|
||||||
|
.as_ref()
|
||||||
|
.map(|node| node.clone_mounted());
|
||||||
|
if let Some(node) = suspended {
|
||||||
|
node.remove_node(&mut *dom, None::<&mut M>, None);
|
||||||
|
}
|
||||||
|
// Replace the rendered nodes with resolved nodes
|
||||||
|
currently_rendered.remove_node(&mut *dom, Some(to), Some(replace_with));
|
||||||
|
|
||||||
|
// Switch to only writing templates
|
||||||
|
only_write_templates(to);
|
||||||
|
|
||||||
|
let children = RenderReturn { node: children };
|
||||||
|
children.mount.take();
|
||||||
|
|
||||||
|
// First always render the children in the background. Rendering the children may cause this boundary to suspend
|
||||||
|
dom.runtime.push_scope(scope_id);
|
||||||
|
children.create(dom, parent, Some(to));
|
||||||
|
dom.runtime.pop_scope();
|
||||||
|
|
||||||
|
// Store the (now mounted) children back into the scope state
|
||||||
|
let scope_state = &mut dom.scopes[scope_id.0];
|
||||||
|
let props = Self::downcast_mut_from_props(&mut *scope_state.props).unwrap();
|
||||||
|
props.children = children.clone().node;
|
||||||
|
scope_state.last_rendered_node = Some(children);
|
||||||
|
props.suspended_nodes = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn diff<M: WriteMutations>(
|
||||||
|
scope_id: ScopeId,
|
||||||
|
dom: &mut VirtualDom,
|
||||||
|
to: Option<&mut M>,
|
||||||
|
) {
|
||||||
|
let scope = &mut dom.scopes[scope_id.0];
|
||||||
|
let myself = Self::downcast_mut_from_props(&mut *scope.props)
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let last_rendered_node = scope.last_rendered_node.as_ref().unwrap().clone_mounted();
|
||||||
|
|
||||||
|
let Self {
|
||||||
|
fallback,
|
||||||
|
children,
|
||||||
|
suspended_nodes,
|
||||||
|
..
|
||||||
|
} = myself;
|
||||||
|
|
||||||
|
let suspense_context = scope
|
||||||
|
.state()
|
||||||
|
.suspense_boundary()
|
||||||
|
.suspense_context()
|
||||||
|
.unwrap()
|
||||||
|
.clone();
|
||||||
|
let suspended = !suspense_context.suspended_futures().is_empty();
|
||||||
|
match (suspended_nodes, suspended) {
|
||||||
|
// We already have suspended nodes that still need to be suspended
|
||||||
|
// Just diff the normal and suspended nodes
|
||||||
|
(Some(suspended_nodes), true) => {
|
||||||
|
let new_suspended_nodes: VNode = RenderReturn { node: children }.into();
|
||||||
|
|
||||||
|
// Diff the placeholder nodes in the dom
|
||||||
|
dom.runtime.suspense_stack.borrow_mut().push(
|
||||||
|
crate::scope_context::SuspenseLocation::InSuspensePlaceholder(
|
||||||
|
suspense_context.clone(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let old_placeholder = last_rendered_node;
|
||||||
|
let new_placeholder = RenderReturn {
|
||||||
|
node: fallback.call(suspense_context),
|
||||||
|
};
|
||||||
|
|
||||||
|
old_placeholder.diff_node(&new_placeholder, dom, to);
|
||||||
|
dom.runtime.suspense_stack.borrow_mut().pop();
|
||||||
|
|
||||||
|
// Set the last rendered node to the placeholder
|
||||||
|
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
|
||||||
|
|
||||||
|
// Diff the suspended nodes in the background
|
||||||
|
dom.runtime.push_scope(scope_id);
|
||||||
|
suspended_nodes.diff_node(&new_suspended_nodes, dom, None::<&mut M>);
|
||||||
|
dom.runtime.pop_scope();
|
||||||
|
|
||||||
|
let props =
|
||||||
|
Self::downcast_mut_from_props(&mut *dom.scopes[scope_id.0].props).unwrap();
|
||||||
|
props.suspended_nodes = Some(new_suspended_nodes);
|
||||||
|
}
|
||||||
|
// We have no suspended nodes, and we are not suspended. Just diff the children like normal
|
||||||
|
(None, false) => {
|
||||||
|
let old_children = last_rendered_node;
|
||||||
|
let new_children = RenderReturn { node: children };
|
||||||
|
|
||||||
|
dom.runtime.push_scope(scope_id);
|
||||||
|
old_children.diff_node(&new_children, dom, to);
|
||||||
|
dom.runtime.pop_scope();
|
||||||
|
|
||||||
|
// Set the last rendered node to the new children
|
||||||
|
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
||||||
|
}
|
||||||
|
// We have no suspended nodes, but we just became suspended. Move the children to the background
|
||||||
|
(None, true) => {
|
||||||
|
let old_children = last_rendered_node;
|
||||||
|
let new_children: VNode = RenderReturn { node: children }.into();
|
||||||
|
|
||||||
|
let new_placeholder = RenderReturn {
|
||||||
|
node: fallback.call(suspense_context.clone()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Move the children to the background
|
||||||
|
let mount = old_children.mount.get();
|
||||||
|
let mount = dom.mounts.get(mount.0).expect("mount should exist");
|
||||||
|
let parent = mount.parent;
|
||||||
|
dom.runtime.push_scope(scope_id);
|
||||||
|
dom.runtime.suspense_stack.borrow_mut().push(
|
||||||
|
crate::scope_context::SuspenseLocation::InSuspensePlaceholder(suspense_context),
|
||||||
|
);
|
||||||
|
old_children.move_node_to_background(
|
||||||
|
std::slice::from_ref(&*new_placeholder),
|
||||||
|
parent,
|
||||||
|
dom,
|
||||||
|
to,
|
||||||
|
);
|
||||||
|
dom.runtime.suspense_stack.borrow_mut().pop();
|
||||||
|
|
||||||
|
// Then diff the new children in the background
|
||||||
|
old_children.diff_node(&new_children, dom, None::<&mut M>);
|
||||||
|
dom.runtime.pop_scope();
|
||||||
|
|
||||||
|
// Set the last rendered node to the new suspense placeholder
|
||||||
|
dom.scopes[scope_id.0].last_rendered_node = Some(new_placeholder);
|
||||||
|
|
||||||
|
let props =
|
||||||
|
Self::downcast_mut_from_props(&mut *dom.scopes[scope_id.0].props).unwrap();
|
||||||
|
props.suspended_nodes = Some(new_children);
|
||||||
|
} // We have suspended nodes, but we just got out of suspense. Move the suspended nodes to the foreground
|
||||||
|
(Some(old_suspended_nodes), false) => {
|
||||||
|
let old_placeholder = last_rendered_node;
|
||||||
|
let new_children = RenderReturn { node: children };
|
||||||
|
|
||||||
|
// First diff the two children nodes in the background
|
||||||
|
dom.runtime.push_scope(scope_id);
|
||||||
|
old_suspended_nodes.diff_node(&new_children, dom, None::<&mut M>);
|
||||||
|
|
||||||
|
// Then replace the placeholder with the new children
|
||||||
|
let mount = old_placeholder.mount.get();
|
||||||
|
let mount = dom.mounts.get(mount.0).expect("mount should exist");
|
||||||
|
let parent = mount.parent;
|
||||||
|
old_placeholder.replace(std::slice::from_ref(&*new_children), parent, dom, to);
|
||||||
|
dom.runtime.pop_scope();
|
||||||
|
|
||||||
|
// Set the last rendered node to the new children
|
||||||
|
dom.scopes[scope_id.0].last_rendered_node = Some(new_children);
|
||||||
|
|
||||||
|
let props =
|
||||||
|
Self::downcast_mut_from_props(&mut *dom.scopes[scope_id.0].props).unwrap();
|
||||||
|
props.suspended_nodes = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_suspended_nodes<M: WriteMutations>(
|
||||||
|
&mut self,
|
||||||
|
dom: &mut VirtualDom,
|
||||||
|
destroy_component_state: bool,
|
||||||
|
) {
|
||||||
|
// Remove the suspended nodes
|
||||||
|
if let Some(node) = self.suspended_nodes.take() {
|
||||||
|
node.remove_node_inner(dom, None::<&mut M>, destroy_component_state, None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
189
packages/core/src/suspense/mod.rs
Normal file
189
packages/core/src/suspense/mod.rs
Normal file
|
@ -0,0 +1,189 @@
|
||||||
|
//! Suspense allows you to render a placeholder while nodes are waiting for data in the background
|
||||||
|
//!
|
||||||
|
//! During suspense on the server:
|
||||||
|
//! - Rebuild once
|
||||||
|
//! - Send page with loading placeholders down to the client
|
||||||
|
//! - loop
|
||||||
|
//! - Poll (only) suspended futures
|
||||||
|
//! - If a scope is marked as dirty and that scope is a suspense boundary, under a suspended boundary, or the suspense placeholder, rerun the scope
|
||||||
|
//! - If it is a different scope, ignore it and warn the user
|
||||||
|
//! - Rerender the scope on the server and send down the nodes under a hidden div with serialized data
|
||||||
|
//!
|
||||||
|
//! During suspense on the web:
|
||||||
|
//! - Rebuild once without running server futures
|
||||||
|
//! - Rehydrate the placeholders that were initially sent down. At this point, no suspense nodes are resolved so the client and server pages should be the same
|
||||||
|
//! - loop
|
||||||
|
//! - Wait for work or suspense data
|
||||||
|
//! - If suspense data comes in
|
||||||
|
//! - replace the suspense placeholder
|
||||||
|
//! - get any data associated with the suspense placeholder and rebuild nodes under the suspense that was resolved
|
||||||
|
//! - rehydrate the suspense placeholders that were at that node
|
||||||
|
//! - If work comes in
|
||||||
|
//! - Just do the work; this may remove suspense placeholders that the server hasn't yet resolved. If we see new data come in from the server about that node, ignore it
|
||||||
|
//!
|
||||||
|
//! Generally suspense placeholders should not be stateful because they are driven from the server. If they are stateful and the client renders something different, hydration will fail.
|
||||||
|
|
||||||
|
mod component;
|
||||||
|
pub use component::*;
|
||||||
|
|
||||||
|
use crate::innerlude::*;
|
||||||
|
use std::{
|
||||||
|
cell::{Cell, Ref, RefCell},
|
||||||
|
fmt::Debug,
|
||||||
|
rc::Rc,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A task that has been suspended which may have an optional loading placeholder
|
||||||
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
|
pub struct SuspendedFuture {
|
||||||
|
origin: ScopeId,
|
||||||
|
task: Task,
|
||||||
|
pub(crate) placeholder: VNode,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuspendedFuture {
|
||||||
|
/// Create a new suspended future
|
||||||
|
pub fn new(task: Task) -> Self {
|
||||||
|
Self {
|
||||||
|
task,
|
||||||
|
origin: current_scope_id().expect("to be in a dioxus runtime"),
|
||||||
|
placeholder: VNode::placeholder(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a placeholder to display while the future is suspended
|
||||||
|
pub fn suspense_placeholder(&self) -> Option<VNode> {
|
||||||
|
if self.placeholder == VNode::placeholder() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(self.placeholder.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set a new placeholder the SuspenseBoundary may use to display while the future is suspended
|
||||||
|
pub fn with_placeholder(mut self, placeholder: VNode) -> Self {
|
||||||
|
self.placeholder = placeholder;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the task that was suspended
|
||||||
|
pub fn task(&self) -> Task {
|
||||||
|
self.task
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clone the future while retaining the mounted information of the future
|
||||||
|
pub(crate) fn clone_mounted(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
task: self.task,
|
||||||
|
origin: self.origin,
|
||||||
|
placeholder: self.placeholder.clone_mounted(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A context with information about suspended components
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct SuspenseContext {
|
||||||
|
inner: Rc<SuspenseBoundaryInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for SuspenseContext {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.inner, &other.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuspenseContext {
|
||||||
|
/// Create a new suspense boundary in a specific scope
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Rc::new(SuspenseBoundaryInner {
|
||||||
|
suspended_tasks: RefCell::new(vec![]),
|
||||||
|
id: Cell::new(ScopeId::ROOT),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mount the context in a specific scope
|
||||||
|
pub(crate) fn mount(&self, scope: ScopeId) {
|
||||||
|
self.inner.id.set(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if there are any suspended tasks
|
||||||
|
pub fn suspended(&self) -> bool {
|
||||||
|
!self.inner.suspended_tasks.borrow().is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a suspended task
|
||||||
|
pub(crate) fn add_suspended_task(&self, task: SuspendedFuture) {
|
||||||
|
self.inner.suspended_tasks.borrow_mut().push(task);
|
||||||
|
self.inner.id.get().needs_update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove a suspended task
|
||||||
|
pub(crate) fn remove_suspended_task(&self, task: Task) {
|
||||||
|
self.inner
|
||||||
|
.suspended_tasks
|
||||||
|
.borrow_mut()
|
||||||
|
.retain(|t| t.task != task);
|
||||||
|
self.inner.id.get().needs_update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get all suspended tasks
|
||||||
|
pub fn suspended_futures(&self) -> Ref<[SuspendedFuture]> {
|
||||||
|
Ref::map(self.inner.suspended_tasks.borrow(), |tasks| {
|
||||||
|
tasks.as_slice()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the first suspended task with a loading placeholder
|
||||||
|
pub fn suspense_placeholder(&self) -> Option<Element> {
|
||||||
|
self.inner
|
||||||
|
.suspended_tasks
|
||||||
|
.borrow()
|
||||||
|
.iter()
|
||||||
|
.find_map(|task| task.suspense_placeholder())
|
||||||
|
.map(std::result::Result::Ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A boundary that will capture any errors from child components
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct SuspenseBoundaryInner {
|
||||||
|
suspended_tasks: RefCell<Vec<SuspendedFuture>>,
|
||||||
|
id: Cell<ScopeId>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Provides context methods to [`Result<T, RenderError>`] to show loading indicators for suspended results
|
||||||
|
///
|
||||||
|
/// This trait is sealed and cannot be implemented outside of dioxus-core
|
||||||
|
pub trait SuspenseExtension<T>: private::Sealed {
|
||||||
|
/// Add a loading indicator if the result is suspended
|
||||||
|
fn with_loading_placeholder(
|
||||||
|
self,
|
||||||
|
display_placeholder: impl FnOnce() -> Element,
|
||||||
|
) -> std::result::Result<T, RenderError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> SuspenseExtension<T> for std::result::Result<T, RenderError> {
|
||||||
|
fn with_loading_placeholder(
|
||||||
|
self,
|
||||||
|
display_placeholder: impl FnOnce() -> Element,
|
||||||
|
) -> std::result::Result<T, RenderError> {
|
||||||
|
if let Err(RenderError::Suspended(suspense)) = self {
|
||||||
|
Err(RenderError::Suspended(suspense.with_placeholder(
|
||||||
|
display_placeholder().unwrap_or_default(),
|
||||||
|
)))
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) mod private {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
pub trait Sealed {}
|
||||||
|
|
||||||
|
impl<T> Sealed for std::result::Result<T, RenderError> {}
|
||||||
|
}
|
|
@ -1,9 +1,12 @@
|
||||||
use crate::innerlude::Effect;
|
use crate::innerlude::Effect;
|
||||||
use crate::innerlude::ScopeOrder;
|
use crate::innerlude::ScopeOrder;
|
||||||
use crate::innerlude::{remove_future, spawn, Runtime};
|
use crate::innerlude::{remove_future, spawn, Runtime};
|
||||||
|
use crate::scope_context::ScopeStatus;
|
||||||
|
use crate::scope_context::SuspenseLocation;
|
||||||
use crate::ScopeId;
|
use crate::ScopeId;
|
||||||
use futures_util::task::ArcWake;
|
use futures_util::task::ArcWake;
|
||||||
use slotmap::DefaultKey;
|
use slotmap::DefaultKey;
|
||||||
|
use std::marker::PhantomData;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::task::Waker;
|
use std::task::Waker;
|
||||||
use std::{cell::Cell, future::Future};
|
use std::{cell::Cell, future::Future};
|
||||||
|
@ -15,9 +18,21 @@ use std::{pin::Pin, task::Poll};
|
||||||
/// `Task` is a unique identifier for a task that has been spawned onto the runtime. It can be used to cancel the task
|
/// `Task` is a unique identifier for a task that has been spawned onto the runtime. It can be used to cancel the task
|
||||||
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
|
||||||
pub struct Task(pub(crate) slotmap::DefaultKey);
|
pub struct Task {
|
||||||
|
pub(crate) id: slotmap::DefaultKey,
|
||||||
|
// We add a raw pointer to make this !Send + !Sync
|
||||||
|
unsend: PhantomData<*const ()>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Task {
|
impl Task {
|
||||||
|
/// Create a task from a raw id
|
||||||
|
pub(crate) const fn from_id(id: slotmap::DefaultKey) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
unsend: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Start a new future on the same thread as the rest of the VirtualDom.
|
/// Start a new future on the same thread as the rest of the VirtualDom.
|
||||||
///
|
///
|
||||||
/// This future will not contribute to suspense resolving, so you should primarily use this for reacting to changes
|
/// This future will not contribute to suspense resolving, so you should primarily use this for reacting to changes
|
||||||
|
@ -51,7 +66,7 @@ impl Task {
|
||||||
/// Check if the task is paused.
|
/// Check if the task is paused.
|
||||||
pub fn paused(&self) -> bool {
|
pub fn paused(&self) -> bool {
|
||||||
Runtime::with(|rt| {
|
Runtime::with(|rt| {
|
||||||
if let Some(task) = rt.tasks.borrow().get(self.0) {
|
if let Some(task) = rt.tasks.borrow().get(self.id) {
|
||||||
!task.active.get()
|
!task.active.get()
|
||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
|
@ -62,7 +77,11 @@ impl Task {
|
||||||
|
|
||||||
/// Wake the task.
|
/// Wake the task.
|
||||||
pub fn wake(&self) {
|
pub fn wake(&self) {
|
||||||
Runtime::with(|rt| _ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self)));
|
Runtime::with(|rt| {
|
||||||
|
_ = rt
|
||||||
|
.sender
|
||||||
|
.unbounded_send(SchedulerMsg::TaskNotified(self.id))
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Poll the task immediately.
|
/// Poll the task immediately.
|
||||||
|
@ -73,10 +92,12 @@ impl Task {
|
||||||
/// Set the task as active or paused.
|
/// Set the task as active or paused.
|
||||||
pub fn set_active(&self, active: bool) {
|
pub fn set_active(&self, active: bool) {
|
||||||
Runtime::with(|rt| {
|
Runtime::with(|rt| {
|
||||||
if let Some(task) = rt.tasks.borrow().get(self.0) {
|
if let Some(task) = rt.tasks.borrow().get(self.id) {
|
||||||
let was_active = task.active.replace(active);
|
let was_active = task.active.replace(active);
|
||||||
if !was_active && active {
|
if !was_active && active {
|
||||||
_ = rt.sender.unbounded_send(SchedulerMsg::TaskNotified(*self));
|
_ = rt
|
||||||
|
.sender
|
||||||
|
.unbounded_send(SchedulerMsg::TaskNotified(self.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -140,10 +161,10 @@ impl Runtime {
|
||||||
let (task, task_id) = {
|
let (task, task_id) = {
|
||||||
let mut tasks = self.tasks.borrow_mut();
|
let mut tasks = self.tasks.borrow_mut();
|
||||||
|
|
||||||
let mut task_id = Task(DefaultKey::default());
|
let mut task_id = Task::from_id(DefaultKey::default());
|
||||||
let mut local_task = None;
|
let mut local_task = None;
|
||||||
tasks.insert_with_key(|key| {
|
tasks.insert_with_key(|key| {
|
||||||
task_id = Task(key);
|
task_id = Task::from_id(key);
|
||||||
|
|
||||||
let new_task = Rc::new(LocalTask {
|
let new_task = Rc::new(LocalTask {
|
||||||
scope,
|
scope,
|
||||||
|
@ -151,10 +172,10 @@ impl Runtime {
|
||||||
parent: self.current_task(),
|
parent: self.current_task(),
|
||||||
task: RefCell::new(Box::pin(task)),
|
task: RefCell::new(Box::pin(task)),
|
||||||
waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
|
waker: futures_util::task::waker(Arc::new(LocalTaskHandle {
|
||||||
id: task_id,
|
id: task_id.id,
|
||||||
tx: self.sender.clone(),
|
tx: self.sender.clone(),
|
||||||
})),
|
})),
|
||||||
ty: Cell::new(ty),
|
ty: RefCell::new(ty),
|
||||||
});
|
});
|
||||||
|
|
||||||
local_task = Some(new_task.clone());
|
local_task = Some(new_task.clone());
|
||||||
|
@ -170,7 +191,7 @@ impl Runtime {
|
||||||
debug_assert!(task.task.try_borrow_mut().is_ok());
|
debug_assert!(task.task.try_borrow_mut().is_ok());
|
||||||
|
|
||||||
self.sender
|
self.sender
|
||||||
.unbounded_send(SchedulerMsg::TaskNotified(task_id))
|
.unbounded_send(SchedulerMsg::TaskNotified(task_id.id))
|
||||||
.expect("Scheduler should exist");
|
.expect("Scheduler should exist");
|
||||||
|
|
||||||
task_id
|
task_id
|
||||||
|
@ -178,11 +199,32 @@ impl Runtime {
|
||||||
|
|
||||||
/// Queue an effect to run after the next render
|
/// Queue an effect to run after the next render
|
||||||
pub(crate) fn queue_effect(&self, id: ScopeId, f: impl FnOnce() + 'static) {
|
pub(crate) fn queue_effect(&self, id: ScopeId, f: impl FnOnce() + 'static) {
|
||||||
|
let effect = Box::new(f) as Box<dyn FnOnce() + 'static>;
|
||||||
|
let Some(scope) = self.get_state(id) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let mut status = scope.status.borrow_mut();
|
||||||
|
match &mut *status {
|
||||||
|
ScopeStatus::Mounted => {
|
||||||
|
self.queue_effect_on_mounted_scope(id, effect);
|
||||||
|
}
|
||||||
|
ScopeStatus::Unmounted { effects_queued, .. } => {
|
||||||
|
effects_queued.push(effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queue an effect to run after the next render without checking if the scope is mounted
|
||||||
|
pub(crate) fn queue_effect_on_mounted_scope(
|
||||||
|
&self,
|
||||||
|
id: ScopeId,
|
||||||
|
f: Box<dyn FnOnce() + 'static>,
|
||||||
|
) {
|
||||||
// Add the effect to the queue of effects to run after the next render for the given scope
|
// Add the effect to the queue of effects to run after the next render for the given scope
|
||||||
let mut effects = self.pending_effects.borrow_mut();
|
let mut effects = self.pending_effects.borrow_mut();
|
||||||
let scope_order = ScopeOrder::new(id.height(), id);
|
let scope_order = ScopeOrder::new(id.height(), id);
|
||||||
match effects.get(&scope_order) {
|
match effects.get(&scope_order) {
|
||||||
Some(effects) => effects.push_back(Box::new(f)),
|
Some(effects) => effects.push_back(f),
|
||||||
None => {
|
None => {
|
||||||
effects.insert(Effect::new(scope_order, f));
|
effects.insert(Effect::new(scope_order, f));
|
||||||
}
|
}
|
||||||
|
@ -196,17 +238,17 @@ impl Runtime {
|
||||||
|
|
||||||
/// Get the parent task of the given task, if it exists
|
/// Get the parent task of the given task, if it exists
|
||||||
pub fn parent_task(&self, task: Task) -> Option<Task> {
|
pub fn parent_task(&self, task: Task) -> Option<Task> {
|
||||||
self.tasks.borrow().get(task.0)?.parent
|
self.tasks.borrow().get(task.id)?.parent
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn task_scope(&self, task: Task) -> Option<ScopeId> {
|
pub(crate) fn task_scope(&self, task: Task) -> Option<ScopeId> {
|
||||||
self.tasks.borrow().get(task.0).map(|t| t.scope)
|
self.tasks.borrow().get(task.id).map(|t| t.scope)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn handle_task_wakeup(&self, id: Task) -> Poll<()> {
|
pub(crate) fn handle_task_wakeup(&self, id: Task) -> Poll<()> {
|
||||||
debug_assert!(Runtime::current().is_some(), "Must be in a dioxus runtime");
|
debug_assert!(Runtime::current().is_some(), "Must be in a dioxus runtime");
|
||||||
|
|
||||||
let task = self.tasks.borrow().get(id.0).cloned();
|
let task = self.tasks.borrow().get(id.id).cloned();
|
||||||
|
|
||||||
// The task was removed from the scheduler, so we can just ignore it
|
// The task was removed from the scheduler, so we can just ignore it
|
||||||
let Some(task) = task else {
|
let Some(task) = task else {
|
||||||
|
@ -221,7 +263,7 @@ impl Runtime {
|
||||||
let mut cx = std::task::Context::from_waker(&task.waker);
|
let mut cx = std::task::Context::from_waker(&task.waker);
|
||||||
|
|
||||||
// update the scope stack
|
// update the scope stack
|
||||||
self.scope_stack.borrow_mut().push(task.scope);
|
self.push_scope(task.scope);
|
||||||
self.rendering.set(false);
|
self.rendering.set(false);
|
||||||
self.current_task.set(Some(id));
|
self.current_task.set(Some(id));
|
||||||
|
|
||||||
|
@ -239,7 +281,7 @@ impl Runtime {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the scope from the stack
|
// Remove the scope from the stack
|
||||||
self.scope_stack.borrow_mut().pop();
|
self.pop_scope();
|
||||||
self.rendering.set(true);
|
self.rendering.set(true);
|
||||||
self.current_task.set(None);
|
self.current_task.set(None);
|
||||||
|
|
||||||
|
@ -250,20 +292,35 @@ impl Runtime {
|
||||||
///
|
///
|
||||||
/// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you
|
/// This does not abort the task, so you'll want to wrap it in an abort handle if that's important to you
|
||||||
pub(crate) fn remove_task(&self, id: Task) -> Option<Rc<LocalTask>> {
|
pub(crate) fn remove_task(&self, id: Task) -> Option<Rc<LocalTask>> {
|
||||||
let task = self.tasks.borrow_mut().remove(id.0);
|
// Remove the task from the task list
|
||||||
|
let task = self.tasks.borrow_mut().remove(id.id);
|
||||||
|
|
||||||
if let Some(task) = &task {
|
if let Some(task) = &task {
|
||||||
if task.suspended() {
|
// Remove the task from suspense
|
||||||
|
if let TaskType::Suspended { boundary } = &*task.ty.borrow() {
|
||||||
self.suspended_tasks.set(self.suspended_tasks.get() - 1);
|
self.suspended_tasks.set(self.suspended_tasks.get() - 1);
|
||||||
|
if let SuspenseLocation::UnderSuspense(boundary) = boundary {
|
||||||
|
boundary.remove_suspended_task(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the task from pending work. We could reuse the slot before the task is polled and discarded so we need to remove it from pending work instead of filtering out dead tasks when we try to poll them
|
||||||
|
if let Some(scope) = self.get_state(task.scope) {
|
||||||
|
let order = ScopeOrder::new(scope.height(), scope.id);
|
||||||
|
if let Some(dirty_tasks) = self.dirty_tasks.borrow_mut().get(&order) {
|
||||||
|
dirty_tasks.remove(id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
task
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if a task should be run during suspense
|
/// Check if a task should be run during suspense
|
||||||
pub(crate) fn task_runs_during_suspense(&self, task: Task) -> bool {
|
pub(crate) fn task_runs_during_suspense(&self, task: Task) -> bool {
|
||||||
let borrow = self.tasks.borrow();
|
let borrow = self.tasks.borrow();
|
||||||
let task: Option<&LocalTask> = borrow.get(task.0).map(|t| &**t);
|
let task: Option<&LocalTask> = borrow.get(task.id).map(|t| &**t);
|
||||||
matches!(task, Some(LocalTask { ty, .. }) if ty.get().runs_during_suspense())
|
matches!(task, Some(LocalTask { ty, .. }) if ty.borrow().runs_during_suspense())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,49 +330,49 @@ pub(crate) struct LocalTask {
|
||||||
parent: Option<Task>,
|
parent: Option<Task>,
|
||||||
task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
task: RefCell<Pin<Box<dyn Future<Output = ()> + 'static>>>,
|
||||||
waker: Waker,
|
waker: Waker,
|
||||||
ty: Cell<TaskType>,
|
ty: RefCell<TaskType>,
|
||||||
active: Cell<bool>,
|
active: Cell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalTask {
|
impl LocalTask {
|
||||||
pub(crate) fn suspend(&self) {
|
/// Suspend the task, returns true if the task was already suspended
|
||||||
self.ty.set(TaskType::Suspended);
|
pub(crate) fn suspend(&self, boundary: SuspenseLocation) -> bool {
|
||||||
}
|
// Make this a suspended task so it runs during suspense
|
||||||
|
let old_type = self.ty.replace(TaskType::Suspended { boundary });
|
||||||
pub(crate) fn suspended(&self) -> bool {
|
matches!(old_type, TaskType::Suspended { .. })
|
||||||
matches!(self.ty.get(), TaskType::Suspended)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone)]
|
||||||
enum TaskType {
|
enum TaskType {
|
||||||
ClientOnly,
|
ClientOnly,
|
||||||
Suspended,
|
Suspended { boundary: SuspenseLocation },
|
||||||
Isomorphic,
|
Isomorphic,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TaskType {
|
impl TaskType {
|
||||||
fn runs_during_suspense(self) -> bool {
|
fn runs_during_suspense(&self) -> bool {
|
||||||
matches!(self, TaskType::Isomorphic | TaskType::Suspended)
|
matches!(self, TaskType::Isomorphic | TaskType::Suspended { .. })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The type of message that can be sent to the scheduler.
|
/// The type of message that can be sent to the scheduler.
|
||||||
///
|
///
|
||||||
/// These messages control how the scheduler will process updates to the UI.
|
/// These messages control how the scheduler will process updates to the UI.
|
||||||
|
#[derive(Debug)]
|
||||||
pub(crate) enum SchedulerMsg {
|
pub(crate) enum SchedulerMsg {
|
||||||
/// Immediate updates from Components that mark them as dirty
|
/// Immediate updates from Components that mark them as dirty
|
||||||
Immediate(ScopeId),
|
Immediate(ScopeId),
|
||||||
|
|
||||||
/// A task has woken and needs to be progressed
|
/// A task has woken and needs to be progressed
|
||||||
TaskNotified(Task),
|
TaskNotified(slotmap::DefaultKey),
|
||||||
|
|
||||||
/// An effect has been queued to run after the next render
|
/// An effect has been queued to run after the next render
|
||||||
EffectQueued,
|
EffectQueued,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LocalTaskHandle {
|
struct LocalTaskHandle {
|
||||||
id: Task,
|
id: slotmap::DefaultKey,
|
||||||
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
tx: futures_channel::mpsc::UnboundedSender<SchedulerMsg>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,22 +2,22 @@
|
||||||
//!
|
//!
|
||||||
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
|
//! This module provides the primary mechanics to create a hook-based, concurrent VDOM for Rust.
|
||||||
|
|
||||||
use crate::Task;
|
use crate::innerlude::{SuspenseBoundaryProps, Work};
|
||||||
|
use crate::properties::RootProps;
|
||||||
|
use crate::root_wrapper::RootScopeWrapper;
|
||||||
use crate::{
|
use crate::{
|
||||||
any_props::AnyProps,
|
|
||||||
arena::ElementId,
|
arena::ElementId,
|
||||||
innerlude::{
|
innerlude::{
|
||||||
DirtyTasks, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState,
|
ElementRef, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VNodeMount, VProps,
|
||||||
VNodeMount, VProps, WriteMutations,
|
WriteMutations,
|
||||||
},
|
},
|
||||||
nodes::RenderReturn,
|
|
||||||
nodes::{Template, TemplateId},
|
nodes::{Template, TemplateId},
|
||||||
runtime::{Runtime, RuntimeGuard},
|
runtime::{Runtime, RuntimeGuard},
|
||||||
scopes::ScopeId,
|
scopes::ScopeId,
|
||||||
AttributeValue, ComponentFunction, Element, Event, Mutations, VNode,
|
AttributeValue, ComponentFunction, Element, Event, Mutations,
|
||||||
};
|
};
|
||||||
|
use crate::{Task, VComponent};
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use rustc_hash::FxHashMap;
|
|
||||||
use slab::Slab;
|
use slab::Slab;
|
||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::{any::Any, rc::Rc};
|
use std::{any::Any, rc::Rc};
|
||||||
|
@ -206,18 +206,25 @@ pub struct VirtualDom {
|
||||||
pub(crate) scopes: Slab<ScopeState>,
|
pub(crate) scopes: Slab<ScopeState>,
|
||||||
|
|
||||||
pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
|
pub(crate) dirty_scopes: BTreeSet<ScopeOrder>,
|
||||||
pub(crate) dirty_tasks: BTreeSet<DirtyTasks>,
|
|
||||||
|
|
||||||
// Maps a template path to a map of byte indexes to templates
|
// Maps a template path to a map of byte indexes to templates
|
||||||
pub(crate) templates: FxHashMap<TemplateId, FxHashMap<usize, Template>>,
|
// if hot reload is enabled, we need to keep track of template overrides
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub(crate) templates: rustc_hash::FxHashMap<TemplateId, rustc_hash::FxHashMap<usize, Template>>,
|
||||||
|
// Otherwise, we just need to keep track of what templates we have registered
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
pub(crate) templates: rustc_hash::FxHashSet<TemplateId>,
|
||||||
|
|
||||||
// Templates changes that are queued for the next render
|
// Templates changes that are queued for the next render
|
||||||
pub(crate) queued_templates: Vec<Template>,
|
pub(crate) queued_templates: Vec<Template>,
|
||||||
|
|
||||||
// The element ids that are used in the renderer
|
// The element ids that are used in the renderer
|
||||||
|
// These mark a specific place in a whole rsx block
|
||||||
pub(crate) elements: Slab<Option<ElementRef>>,
|
pub(crate) elements: Slab<Option<ElementRef>>,
|
||||||
|
|
||||||
// Once nodes are mounted, the information about where they are mounted is stored here
|
// Once nodes are mounted, the information about where they are mounted is stored here
|
||||||
|
// We need to store this information on the virtual dom so that we know what nodes are mounted where when we bubble events
|
||||||
|
// Each mount is associated with a whole rsx block. [`VirtualDom::elements`] link to a specific node in the block
|
||||||
pub(crate) mounts: Slab<VNodeMount>,
|
pub(crate) mounts: Slab<VNodeMount>,
|
||||||
|
|
||||||
pub(crate) runtime: Rc<Runtime>,
|
pub(crate) runtime: Rc<Runtime>,
|
||||||
|
@ -297,7 +304,13 @@ impl VirtualDom {
|
||||||
root: impl ComponentFunction<P, M>,
|
root: impl ComponentFunction<P, M>,
|
||||||
root_props: P,
|
root_props: P,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self::new_with_component(VProps::new(root, |_, _| true, root_props, "root"))
|
let render_fn = root.id();
|
||||||
|
let props = VProps::new(root, |_, _| true, root_props, "Root");
|
||||||
|
Self::new_with_component(VComponent {
|
||||||
|
name: "root",
|
||||||
|
render_fn,
|
||||||
|
props: Box::new(props),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new virtualdom and build it immediately
|
/// Create a new virtualdom and build it immediately
|
||||||
|
@ -309,7 +322,7 @@ impl VirtualDom {
|
||||||
|
|
||||||
/// Create a new VirtualDom from something that implements [`AnyProps`]
|
/// Create a new VirtualDom from something that implements [`AnyProps`]
|
||||||
#[instrument(skip(root), level = "trace", name = "VirtualDom::new")]
|
#[instrument(skip(root), level = "trace", name = "VirtualDom::new")]
|
||||||
pub(crate) fn new_with_component(root: impl AnyProps + 'static) -> Self {
|
pub(crate) fn new_with_component(root: VComponent) -> Self {
|
||||||
let (tx, rx) = futures_channel::mpsc::unbounded();
|
let (tx, rx) = futures_channel::mpsc::unbounded();
|
||||||
|
|
||||||
let mut dom = Self {
|
let mut dom = Self {
|
||||||
|
@ -317,18 +330,19 @@ impl VirtualDom {
|
||||||
runtime: Runtime::new(tx),
|
runtime: Runtime::new(tx),
|
||||||
scopes: Default::default(),
|
scopes: Default::default(),
|
||||||
dirty_scopes: Default::default(),
|
dirty_scopes: Default::default(),
|
||||||
dirty_tasks: Default::default(),
|
|
||||||
templates: Default::default(),
|
templates: Default::default(),
|
||||||
queued_templates: Default::default(),
|
queued_templates: Default::default(),
|
||||||
elements: Default::default(),
|
elements: Default::default(),
|
||||||
mounts: Default::default(),
|
mounts: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let root = dom.new_scope(Box::new(root), "app");
|
let root = VProps::new(
|
||||||
|
RootScopeWrapper,
|
||||||
// Unlike react, we provide a default error boundary that just renders the error as a string
|
|_, _| true,
|
||||||
root.state()
|
RootProps(root),
|
||||||
.provide_context(Rc::new(ErrorBoundary::new_in_scope(ScopeId::ROOT)));
|
"RootWrapper",
|
||||||
|
);
|
||||||
|
dom.new_scope(Box::new(root), "app");
|
||||||
|
|
||||||
// the root element is always given element ID 0 since it's the container for the entire tree
|
// the root element is always given element ID 0 since it's the container for the entire tree
|
||||||
dom.elements.insert(None);
|
dom.elements.insert(None);
|
||||||
|
@ -404,7 +418,7 @@ impl VirtualDom {
|
||||||
tracing::Level::TRACE,
|
tracing::Level::TRACE,
|
||||||
"Marking task {:?} (spawned in {:?}) as dirty",
|
"Marking task {:?} (spawned in {:?}) as dirty",
|
||||||
task,
|
task,
|
||||||
scope.id
|
scope.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
let order = ScopeOrder::new(scope.height(), scope.id);
|
let order = ScopeOrder::new(scope.height(), scope.id);
|
||||||
|
@ -485,7 +499,7 @@ impl VirtualDom {
|
||||||
SchedulerMsg::TaskNotified(id) => {
|
SchedulerMsg::TaskNotified(id) => {
|
||||||
// Instead of running the task immediately, we insert it into the runtime's task queue.
|
// Instead of running the task immediately, we insert it into the runtime's task queue.
|
||||||
// The task may be marked dirty at the same time as the scope that owns the task is dropped.
|
// The task may be marked dirty at the same time as the scope that owns the task is dropped.
|
||||||
self.mark_task_dirty(id);
|
self.mark_task_dirty(Task::from_id(id));
|
||||||
}
|
}
|
||||||
SchedulerMsg::EffectQueued => {}
|
SchedulerMsg::EffectQueued => {}
|
||||||
};
|
};
|
||||||
|
@ -497,7 +511,7 @@ impl VirtualDom {
|
||||||
while let Ok(Some(msg)) = self.rx.try_next() {
|
while let Ok(Some(msg)) = self.rx.try_next() {
|
||||||
match msg {
|
match msg {
|
||||||
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
SchedulerMsg::Immediate(id) => self.mark_dirty(id),
|
||||||
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(task),
|
SchedulerMsg::TaskNotified(task) => self.mark_task_dirty(Task::from_id(task)),
|
||||||
SchedulerMsg::EffectQueued => {}
|
SchedulerMsg::EffectQueued => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -524,24 +538,18 @@ impl VirtualDom {
|
||||||
|
|
||||||
// Keep polling tasks until there are no more effects or tasks to run
|
// Keep polling tasks until there are no more effects or tasks to run
|
||||||
// Or until we have no more dirty scopes
|
// Or until we have no more dirty scopes
|
||||||
while !self.dirty_tasks.is_empty() || !self.runtime.pending_effects.borrow().is_empty() {
|
while !self.runtime.dirty_tasks.borrow().is_empty()
|
||||||
|
|| !self.runtime.pending_effects.borrow().is_empty()
|
||||||
|
{
|
||||||
// Next, run any queued tasks
|
// Next, run any queued tasks
|
||||||
// We choose not to poll the deadline since we complete pretty quickly anyways
|
// We choose not to poll the deadline since we complete pretty quickly anyways
|
||||||
while let Some(task) = self.pop_task() {
|
while let Some(task) = self.pop_task() {
|
||||||
// Then poll any tasks that might be pending
|
let _ = self.runtime.handle_task_wakeup(task);
|
||||||
let mut tasks = task.tasks_queued.into_inner();
|
|
||||||
while let Some(task) = tasks.pop_front() {
|
|
||||||
let _ = self.runtime.handle_task_wakeup(task);
|
|
||||||
|
|
||||||
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
||||||
self.queue_events();
|
self.queue_events();
|
||||||
if self.has_dirty_scopes() {
|
if self.has_dirty_scopes() {
|
||||||
// requeue any remaining tasks
|
return;
|
||||||
for task in tasks {
|
|
||||||
self.mark_task_dirty(task);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -565,41 +573,45 @@ impl VirtualDom {
|
||||||
/// This will only replace the parent template, not any nested templates.
|
/// This will only replace the parent template, not any nested templates.
|
||||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::replace_template")]
|
||||||
pub fn replace_template(&mut self, template: Template) {
|
pub fn replace_template(&mut self, template: Template) {
|
||||||
self.register_template_first_byte_index(template);
|
// we only replace templates if hot reloading is enabled
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
self.register_template_first_byte_index(template);
|
||||||
|
|
||||||
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
// iterating a slab is very inefficient, but this is a rare operation that will only happen during development so it's fine
|
||||||
let mut dirty = Vec::new();
|
let mut dirty = Vec::new();
|
||||||
for (id, scope) in self.scopes.iter() {
|
for (id, scope) in self.scopes.iter() {
|
||||||
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
|
// Recurse into the dynamic nodes of the existing mounted node to see if the template is alive in the tree
|
||||||
fn check_node_for_templates(node: &VNode, template: Template) -> bool {
|
fn check_node_for_templates(node: &crate::VNode, template: Template) -> bool {
|
||||||
let this_template_name = node.template.get().name.rsplit_once(':').unwrap().0;
|
let this_template_name = node.template.get().name.rsplit_once(':').unwrap().0;
|
||||||
|
|
||||||
if this_template_name == template.name.rsplit_once(':').unwrap().0 {
|
if this_template_name == template.name.rsplit_once(':').unwrap().0 {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for dynamic in node.dynamic_nodes.iter() {
|
for dynamic in node.dynamic_nodes.iter() {
|
||||||
if let crate::DynamicNode::Fragment(nodes) = dynamic {
|
if let crate::DynamicNode::Fragment(nodes) = dynamic {
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
if check_node_for_templates(node, template) {
|
if check_node_for_templates(node, template) {
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
if let Some(sync) = scope.try_root_node() {
|
||||||
}
|
if check_node_for_templates(sync, template) {
|
||||||
|
dirty.push(ScopeId(id));
|
||||||
if let Some(RenderReturn::Ready(sync)) = scope.try_root_node() {
|
}
|
||||||
if check_node_for_templates(sync, template) {
|
|
||||||
dirty.push(ScopeId(id));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
for dirty in dirty {
|
for dirty in dirty {
|
||||||
self.mark_dirty(dirty);
|
self.mark_dirty(dirty);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -647,8 +659,10 @@ impl VirtualDom {
|
||||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||||
let new_nodes = self.run_scope(ScopeId::ROOT);
|
let new_nodes = self.run_scope(ScopeId::ROOT);
|
||||||
|
|
||||||
|
self.scopes[ScopeId::ROOT.0].last_rendered_node = Some(new_nodes.clone());
|
||||||
|
|
||||||
// Rebuilding implies we append the created elements to the root
|
// Rebuilding implies we append the created elements to the root
|
||||||
let m = self.create_scope(to, ScopeId::ROOT, new_nodes, None);
|
let m = self.create_scope(Some(to), ScopeId::ROOT, new_nodes, None);
|
||||||
|
|
||||||
to.append_children(ElementId(0), m);
|
to.append_children(ElementId(0), m);
|
||||||
}
|
}
|
||||||
|
@ -666,22 +680,17 @@ impl VirtualDom {
|
||||||
|
|
||||||
// Next, diff any dirty scopes
|
// Next, diff any dirty scopes
|
||||||
// We choose not to poll the deadline since we complete pretty quickly anyways
|
// We choose not to poll the deadline since we complete pretty quickly anyways
|
||||||
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||||
while let Some(work) = self.pop_work() {
|
while let Some(work) = self.pop_work() {
|
||||||
{
|
match work {
|
||||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
Work::PollTask(task) => {
|
||||||
|
_ = self.runtime.handle_task_wakeup(task);
|
||||||
// Then, poll any tasks that might be pending in the scope
|
// Make sure we process any new events
|
||||||
for task in work.tasks {
|
self.queue_events();
|
||||||
let _ = self.runtime.handle_task_wakeup(task);
|
|
||||||
}
|
}
|
||||||
|
Work::RerunScope(scope) => {
|
||||||
self.queue_events();
|
// If the scope is dirty, run the scope and get the mutations
|
||||||
|
self.run_and_diff_scope(Some(to), scope.id);
|
||||||
// If the scope is dirty, run the scope and get the mutations
|
|
||||||
if work.rerun_scope {
|
|
||||||
let new_nodes = self.run_scope(work.scope.id);
|
|
||||||
|
|
||||||
self.diff_scope(to, work.scope.id, new_nodes);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -705,70 +714,127 @@ impl VirtualDom {
|
||||||
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
|
#[instrument(skip(self), level = "trace", name = "VirtualDom::wait_for_suspense")]
|
||||||
pub async fn wait_for_suspense(&mut self) {
|
pub async fn wait_for_suspense(&mut self) {
|
||||||
loop {
|
loop {
|
||||||
if self.runtime.suspended_tasks.get() == 0 {
|
if !self.suspended_tasks_remaining() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for a work to be ready (IE new suspense leaves to pop up)
|
self.wait_for_suspense_work().await;
|
||||||
'wait_for_work: loop {
|
|
||||||
// Process all events - Scopes are marked dirty, etc
|
|
||||||
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
|
|
||||||
self.queue_events();
|
|
||||||
|
|
||||||
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
|
self.render_suspense_immediate().await;
|
||||||
if self.has_dirty_scopes() {
|
}
|
||||||
break;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
{
|
/// Check if there are any suspended tasks remaining
|
||||||
// Make sure we set the runtime since we're running user code
|
pub fn suspended_tasks_remaining(&self) -> bool {
|
||||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
self.runtime.suspended_tasks.get() > 0
|
||||||
// Next, run any queued tasks
|
}
|
||||||
// We choose not to poll the deadline since we complete pretty quickly anyways
|
|
||||||
while let Some(task) = self.pop_task() {
|
|
||||||
// Then poll any tasks that might be pending
|
|
||||||
let mut tasks = task.tasks_queued.into_inner();
|
|
||||||
while let Some(task) = tasks.pop_front() {
|
|
||||||
if self.runtime.task_runs_during_suspense(task) {
|
|
||||||
let _ = self.runtime.handle_task_wakeup(task);
|
|
||||||
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
|
||||||
self.queue_events();
|
|
||||||
if self.has_dirty_scopes() {
|
|
||||||
// requeue any remaining tasks
|
|
||||||
for task in tasks {
|
|
||||||
self.mark_task_dirty(task);
|
|
||||||
}
|
|
||||||
break 'wait_for_work;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.wait_for_event().await;
|
/// Wait for the scheduler to have any work that should be run during suspense.
|
||||||
|
pub async fn wait_for_suspense_work(&mut self) {
|
||||||
|
// Wait for a work to be ready (IE new suspense leaves to pop up)
|
||||||
|
loop {
|
||||||
|
// Process all events - Scopes are marked dirty, etc
|
||||||
|
// Sometimes when wakers fire we get a slew of updates at once, so its important that we drain this completely
|
||||||
|
self.queue_events();
|
||||||
|
|
||||||
|
// Now that we have collected all queued work, we should check if we have any dirty scopes. If there are not, then we can poll any queued futures
|
||||||
|
if self.has_dirty_scopes() {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
|
{
|
||||||
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
// Make sure we set the runtime since we're running user code
|
||||||
while let Some(work) = self.pop_work() {
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||||
// Then, poll any tasks that might be pending in the scope
|
// Next, run any queued tasks
|
||||||
for task in work.tasks {
|
// We choose not to poll the deadline since we complete pretty quickly anyways
|
||||||
|
let mut tasks_polled = 0;
|
||||||
|
while let Some(task) = self.pop_task() {
|
||||||
|
if self.runtime.task_runs_during_suspense(task) {
|
||||||
|
let _ = self.runtime.handle_task_wakeup(task);
|
||||||
|
// Running that task, may mark a scope higher up as dirty. If it does, return from the function early
|
||||||
|
self.queue_events();
|
||||||
|
if self.has_dirty_scopes() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tasks_polled += 1;
|
||||||
|
// Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
|
||||||
|
if tasks_polled > 32 {
|
||||||
|
yield_now().await;
|
||||||
|
tasks_polled = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.wait_for_event().await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render any dirty scopes immediately, but don't poll any futures that are client only on that scope
|
||||||
|
/// Returns a list of suspense boundaries that were resolved
|
||||||
|
pub async fn render_suspense_immediate(&mut self) -> Vec<ScopeId> {
|
||||||
|
// Queue any new events before we start working
|
||||||
|
self.queue_events();
|
||||||
|
|
||||||
|
let mut resolved_scopes = Vec::new();
|
||||||
|
|
||||||
|
// Render whatever work needs to be rendered, unlocking new futures and suspense leaves
|
||||||
|
let _runtime = RuntimeGuard::new(self.runtime.clone());
|
||||||
|
|
||||||
|
let mut work_done = 0;
|
||||||
|
while let Some(work) = self.pop_work() {
|
||||||
|
match work {
|
||||||
|
Work::PollTask(task) => {
|
||||||
// During suspense, we only want to run tasks that are suspended
|
// During suspense, we only want to run tasks that are suspended
|
||||||
if self.runtime.task_runs_during_suspense(task) {
|
if self.runtime.task_runs_during_suspense(task) {
|
||||||
let _ = self.runtime.handle_task_wakeup(task);
|
let _ = self.runtime.handle_task_wakeup(task);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Work::RerunScope(scope) => {
|
||||||
|
if self
|
||||||
|
.runtime
|
||||||
|
.get_state(scope.id)
|
||||||
|
.filter(|scope| scope.should_run_during_suspense())
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
let scope_state = self.get_scope(scope.id).unwrap();
|
||||||
|
let was_suspended =
|
||||||
|
SuspenseBoundaryProps::downcast_ref_from_props(&*scope_state.props)
|
||||||
|
.filter(|props| props.suspended())
|
||||||
|
.is_some();
|
||||||
|
// If the scope is dirty, run the scope and get the mutations
|
||||||
|
self.run_and_diff_scope(None::<&mut NoOpMutations>, scope.id);
|
||||||
|
let scope_state = self.get_scope(scope.id).unwrap();
|
||||||
|
let is_now_suspended =
|
||||||
|
SuspenseBoundaryProps::downcast_ref_from_props(&*scope_state.props)
|
||||||
|
.filter(|props| props.suspended())
|
||||||
|
.is_some();
|
||||||
|
|
||||||
self.queue_events();
|
if is_now_suspended {
|
||||||
|
resolved_scopes.retain(|&id| id != scope.id);
|
||||||
// If the scope is dirty, run the scope and get the mutations
|
} else if was_suspended {
|
||||||
if work.rerun_scope {
|
resolved_scopes.push(scope.id);
|
||||||
let new_nodes = self.run_scope(work.scope.id);
|
}
|
||||||
|
} else {
|
||||||
self.diff_scope(&mut NoOpMutations, work.scope.id, new_nodes);
|
tracing::warn!(
|
||||||
|
"Scope {:?} was marked as dirty, but will not rerun during suspense. Only nodes that are under a suspense boundary rerun during suspense",
|
||||||
|
scope.id
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Queue any new events
|
||||||
|
self.queue_events();
|
||||||
|
work_done += 1;
|
||||||
|
// Once we have polled a few tasks, we manually yield to the scheduler to give it a chance to run other pending work
|
||||||
|
if work_done > 32 {
|
||||||
|
yield_now().await;
|
||||||
|
work_done = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolved_scopes.sort_by_key(|&id| self.runtime.get_state(id).unwrap().height);
|
||||||
|
resolved_scopes
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current runtime
|
/// Get the current runtime
|
||||||
|
@ -817,7 +883,11 @@ impl VirtualDom {
|
||||||
while let Some(path) = parent {
|
while let Some(path) = parent {
|
||||||
let mut listeners = vec![];
|
let mut listeners = vec![];
|
||||||
|
|
||||||
let el_ref = &self.mounts[path.mount.0].node;
|
let Some(mount) = self.mounts.get(path.mount.0) else {
|
||||||
|
// If the node is suspended and not mounted, we can just ignore the event
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let el_ref = &mount.node;
|
||||||
let node_template = el_ref.template.get();
|
let node_template = el_ref.template.get();
|
||||||
let target_path = path.path;
|
let target_path = path.path;
|
||||||
|
|
||||||
|
@ -827,9 +897,7 @@ impl VirtualDom {
|
||||||
|
|
||||||
for attr in attrs.iter() {
|
for attr in attrs.iter() {
|
||||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||||
if attr.name.trim_start_matches("on") == name
|
if attr.name.get(2..) == Some(name) && target_path.is_decendant(this_path) {
|
||||||
&& target_path.is_decendant(this_path)
|
|
||||||
{
|
|
||||||
listeners.push(&attr.value);
|
listeners.push(&attr.value);
|
||||||
|
|
||||||
// Break if this is the exact target element.
|
// Break if this is the exact target element.
|
||||||
|
@ -874,7 +942,11 @@ impl VirtualDom {
|
||||||
name = "VirtualDom::handle_non_bubbling_event"
|
name = "VirtualDom::handle_non_bubbling_event"
|
||||||
)]
|
)]
|
||||||
fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
|
fn handle_non_bubbling_event(&mut self, node: ElementRef, name: &str, uievent: Event<dyn Any>) {
|
||||||
let el_ref = &self.mounts[node.mount.0].node;
|
let Some(mount) = self.mounts.get(node.mount.0) else {
|
||||||
|
// If the node is suspended and not mounted, we can just ignore the event
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let el_ref = &mount.node;
|
||||||
let node_template = el_ref.template.get();
|
let node_template = el_ref.template.get();
|
||||||
let target_path = node.path;
|
let target_path = node.path;
|
||||||
|
|
||||||
|
@ -884,7 +956,7 @@ impl VirtualDom {
|
||||||
for attr in attrs.iter() {
|
for attr in attrs.iter() {
|
||||||
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
// Remove the "on" prefix if it exists, TODO, we should remove this and settle on one
|
||||||
// Only call the listener if this is the exact target element.
|
// Only call the listener if this is the exact target element.
|
||||||
if attr.name.trim_start_matches("on") == name && target_path == this_path {
|
if attr.name.get(2..) == Some(name) && target_path == this_path {
|
||||||
if let AttributeValue::Listener(listener) = &attr.value {
|
if let AttributeValue::Listener(listener) = &attr.value {
|
||||||
self.runtime.rendering.set(false);
|
self.runtime.rendering.set(false);
|
||||||
listener.call(uievent.clone());
|
listener.call(uievent.clone());
|
||||||
|
@ -907,3 +979,18 @@ impl Drop for VirtualDom {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Yield control back to the async scheduler. This is used to give the scheduler a chance to run other pending work. Or cancel the task if the client has disconnected.
|
||||||
|
async fn yield_now() {
|
||||||
|
let mut yielded = false;
|
||||||
|
std::future::poll_fn::<(), _>(move |cx| {
|
||||||
|
if !yielded {
|
||||||
|
cx.waker().wake_by_ref();
|
||||||
|
yielded = true;
|
||||||
|
std::task::Poll::Pending
|
||||||
|
} else {
|
||||||
|
std::task::Poll::Ready(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ use dioxus::prelude::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn attrs_cycle() {
|
fn attrs_cycle() {
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(|| {
|
let mut dom = VirtualDom::new(|| {
|
||||||
let id = generation();
|
let id = generation();
|
||||||
match id % 2 {
|
match id % 2 {
|
||||||
|
@ -26,7 +28,7 @@ fn attrs_cycle() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -38,7 +40,7 @@ fn attrs_cycle() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -47,7 +49,7 @@ fn attrs_cycle() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -70,7 +72,7 @@ fn attrs_cycle() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// we take the node taken by attributes since we reused it
|
// we take the node taken by attributes since we reused it
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
|
|
@ -22,7 +22,7 @@ fn bubbles_error() {
|
||||||
let _edits = dom.rebuild_to_vec().santize();
|
let _edits = dom.rebuild_to_vec().santize();
|
||||||
}
|
}
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
|
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ async fn child_futures_drop_first() {
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
// Here the parent and task could resolve at the same time, but because the task is in the child, dioxus should run the parent first because the child might be dropped
|
// Here the parent and task could resolve at the same time, but because the task is in the child, dioxus should run the parent first because the child might be dropped
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
|
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = dom.wait_for_work() => {}
|
_ = dom.wait_for_work() => {}
|
||||||
|
|
|
@ -27,26 +27,26 @@ fn state_shares() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
dom.in_runtime(|| {
|
dom.in_runtime(|| {
|
||||||
assert_eq!(ScopeId::ROOT.consume_context::<i32>().unwrap(), 1);
|
assert_eq!(ScopeId::APP.consume_context::<i32>().unwrap(), 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
dom.in_runtime(|| {
|
dom.in_runtime(|| {
|
||||||
assert_eq!(ScopeId::ROOT.consume_context::<i32>().unwrap(), 2);
|
assert_eq!(ScopeId::APP.consume_context::<i32>().unwrap(), 2);
|
||||||
});
|
});
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId(2));
|
dom.mark_dirty(ScopeId(ScopeId::APP.0 + 2));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[SetText { value: "Value is 2".to_string(), id: ElementId(1,) },]
|
[SetText { value: "Value is 2".to_string(), id: ElementId(1,) },]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
dom.mark_dirty(ScopeId(2));
|
dom.mark_dirty(ScopeId(ScopeId::APP.0 + 2));
|
||||||
let edits = dom.render_immediate_to_vec();
|
let edits = dom.render_immediate_to_vec();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.santize().edits,
|
edits.santize().edits,
|
||||||
|
|
|
@ -21,7 +21,7 @@ fn cycling_elements() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -31,7 +31,7 @@ fn cycling_elements() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint
|
// notice that the IDs cycle back to ElementId(1), preserving a minimal memory footprint
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -40,7 +40,7 @@ fn cycling_elements() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
|
|
@ -72,7 +72,7 @@ fn component_swap() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -81,7 +81,7 @@ fn component_swap() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -90,7 +90,7 @@ fn component_swap() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
|
|
@ -12,19 +12,19 @@ fn text_diff() {
|
||||||
let mut vdom = VirtualDom::new(app);
|
let mut vdom = VirtualDom::new(app);
|
||||||
vdom.rebuild(&mut NoOpMutations);
|
vdom.rebuild(&mut NoOpMutations);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().edits,
|
vdom.render_immediate_to_vec().edits,
|
||||||
[SetText { value: "hello 1".to_string(), id: ElementId(2) }]
|
[SetText { value: "hello 1".to_string(), id: ElementId(2) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().edits,
|
vdom.render_immediate_to_vec().edits,
|
||||||
[SetText { value: "hello 2".to_string(), id: ElementId(2) }]
|
[SetText { value: "hello 2".to_string(), id: ElementId(2) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().edits,
|
vdom.render_immediate_to_vec().edits,
|
||||||
[SetText { value: "hello 3".to_string(), id: ElementId(2) }]
|
[SetText { value: "hello 3".to_string(), id: ElementId(2) }]
|
||||||
|
@ -46,7 +46,7 @@ fn element_swap() {
|
||||||
let mut vdom = VirtualDom::new(app);
|
let mut vdom = VirtualDom::new(app);
|
||||||
vdom.rebuild(&mut NoOpMutations);
|
vdom.rebuild(&mut NoOpMutations);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().santize().edits,
|
vdom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -55,7 +55,7 @@ fn element_swap() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().santize().edits,
|
vdom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -64,7 +64,7 @@ fn element_swap() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().santize().edits,
|
vdom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -73,7 +73,7 @@ fn element_swap() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().santize().edits,
|
vdom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -126,7 +126,7 @@ fn attribute_diff() {
|
||||||
let mut vdom = VirtualDom::new(app);
|
let mut vdom = VirtualDom::new(app);
|
||||||
vdom.rebuild(&mut NoOpMutations);
|
vdom.rebuild(&mut NoOpMutations);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().santize().edits,
|
vdom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -145,7 +145,7 @@ fn attribute_diff() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().santize().edits,
|
vdom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -166,7 +166,7 @@ fn attribute_diff() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
vdom.render_immediate_to_vec().santize().edits,
|
vdom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -195,7 +195,7 @@ fn diff_empty() {
|
||||||
let mut vdom = VirtualDom::new(app);
|
let mut vdom = VirtualDom::new(app);
|
||||||
vdom.rebuild(&mut NoOpMutations);
|
vdom.rebuild(&mut NoOpMutations);
|
||||||
|
|
||||||
vdom.mark_dirty(ScopeId::ROOT);
|
vdom.mark_dirty(ScopeId::APP);
|
||||||
let edits = vdom.render_immediate_to_vec().santize().edits;
|
let edits = vdom.render_immediate_to_vec().santize().edits;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -39,7 +39,7 @@ fn keyed_diffing_out_of_order() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().edits,
|
dom.render_immediate_to_vec().edits,
|
||||||
[
|
[
|
||||||
|
@ -64,7 +64,7 @@ fn keyed_diffing_out_of_order_adds() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().edits,
|
dom.render_immediate_to_vec().edits,
|
||||||
[
|
[
|
||||||
|
@ -90,7 +90,7 @@ fn keyed_diffing_out_of_order_adds_3() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().edits,
|
dom.render_immediate_to_vec().edits,
|
||||||
[
|
[
|
||||||
|
@ -116,7 +116,7 @@ fn keyed_diffing_out_of_order_adds_4() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().edits,
|
dom.render_immediate_to_vec().edits,
|
||||||
[
|
[
|
||||||
|
@ -142,7 +142,7 @@ fn keyed_diffing_out_of_order_adds_5() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().edits,
|
dom.render_immediate_to_vec().edits,
|
||||||
[
|
[
|
||||||
|
@ -167,7 +167,7 @@ fn keyed_diffing_additions() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -192,7 +192,7 @@ fn keyed_diffing_additions_and_moves_on_ends() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -222,7 +222,7 @@ fn keyed_diffing_additions_and_moves_in_middle() {
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
// LIS: 4, 5, 6
|
// LIS: 4, 5, 6
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -256,7 +256,7 @@ fn controlled_keyed_diffing_out_of_order() {
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
// LIS: 5, 6
|
// LIS: 5, 6
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -289,7 +289,7 @@ fn controlled_keyed_diffing_out_of_order_max_test() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -318,7 +318,7 @@ fn remove_list() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -343,7 +343,7 @@ fn no_common_keys() {
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
|
|
@ -27,7 +27,7 @@ fn list_creates_one_by_one() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rendering the first item should replace the placeholder with an element
|
// Rendering the first item should replace the placeholder with an element
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -38,7 +38,7 @@ fn list_creates_one_by_one() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Rendering the next item should insert after the previous
|
// Rendering the next item should insert after the previous
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -49,7 +49,7 @@ fn list_creates_one_by_one() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// ... and again!
|
// ... and again!
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -60,7 +60,7 @@ fn list_creates_one_by_one() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// once more
|
// once more
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -107,14 +107,14 @@ fn removes_one_by_one() {
|
||||||
|
|
||||||
// Remove div(3)
|
// Remove div(3)
|
||||||
// Rendering the first item should replace the placeholder with an element
|
// Rendering the first item should replace the placeholder with an element
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[Remove { id: ElementId(6) }]
|
[Remove { id: ElementId(6) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Remove div(2)
|
// Remove div(2)
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[Remove { id: ElementId(4) }]
|
[Remove { id: ElementId(4) }]
|
||||||
|
@ -122,7 +122,7 @@ fn removes_one_by_one() {
|
||||||
|
|
||||||
// Remove div(1) and replace with a placeholder
|
// Remove div(1) and replace with a placeholder
|
||||||
// todo: this should just be a remove with no placeholder
|
// todo: this should just be a remove with no placeholder
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -133,16 +133,16 @@ fn removes_one_by_one() {
|
||||||
|
|
||||||
// load the 3 and replace the placeholder
|
// load the 3 and replace the placeholder
|
||||||
// todo: this should actually be append to, but replace placeholder is fine for now
|
// todo: this should actually be append to, but replace placeholder is fine for now
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(2) },
|
||||||
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(3) },
|
HydrateText { path: &[0], value: "0".to_string(), id: ElementId(6) },
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(5) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(8) },
|
||||||
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(6) },
|
HydrateText { path: &[0], value: "1".to_string(), id: ElementId(9) },
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(7) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(10) },
|
||||||
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(8) },
|
HydrateText { path: &[0], value: "2".to_string(), id: ElementId(11) },
|
||||||
ReplaceWith { id: ElementId(4), m: 3 }
|
ReplaceWith { id: ElementId(4), m: 3 }
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
@ -170,7 +170,7 @@ fn list_shrink_multiroot() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -182,7 +182,7 @@ fn list_shrink_multiroot() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -194,7 +194,7 @@ fn list_shrink_multiroot() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -249,19 +249,19 @@ fn removes_one_by_one_multiroot() {
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
|
[Remove { id: ElementId(10) }, Remove { id: ElementId(12) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
|
[Remove { id: ElementId(6) }, Remove { id: ElementId(8) }]
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dom.render_immediate_to_vec().santize().edits,
|
dom.render_immediate_to_vec().santize().edits,
|
||||||
[
|
[
|
||||||
|
@ -330,7 +330,7 @@ fn remove_many() {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
let edits = dom.render_immediate_to_vec().santize();
|
let edits = dom.render_immediate_to_vec().santize();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.edits,
|
edits.edits,
|
||||||
|
@ -343,7 +343,7 @@ fn remove_many() {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
let edits = dom.render_immediate_to_vec().santize();
|
let edits = dom.render_immediate_to_vec().santize();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.edits,
|
edits.edits,
|
||||||
|
@ -362,7 +362,7 @@ fn remove_many() {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
let edits = dom.render_immediate_to_vec().santize();
|
let edits = dom.render_immediate_to_vec().santize();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.edits,
|
edits.edits,
|
||||||
|
@ -378,13 +378,13 @@ fn remove_many() {
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
let edits = dom.render_immediate_to_vec().santize();
|
let edits = dom.render_immediate_to_vec().santize();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
edits.edits,
|
edits.edits,
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(2,) },
|
||||||
HydrateText { path: &[0,], value: "hello 0".to_string(), id: ElementId(3,) },
|
HydrateText { path: &[0,], value: "hello 0".to_string(), id: ElementId(1,) },
|
||||||
ReplaceWith { id: ElementId(11,), m: 1 },
|
ReplaceWith { id: ElementId(11,), m: 1 },
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
|
@ -20,13 +20,13 @@ fn app() -> Element {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn NoneChild() -> Element {
|
fn NoneChild() -> Element {
|
||||||
None
|
VNode::empty()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ThrowChild() -> Element {
|
fn ThrowChild() -> Element {
|
||||||
Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd")).throw()?;
|
Err(std::io::Error::new(std::io::ErrorKind::AddrInUse, "asd"))?;
|
||||||
|
|
||||||
let _g: i32 = "123123".parse().throw()?;
|
let _g: i32 = "123123".parse()?;
|
||||||
|
|
||||||
rsx! { div {} }
|
rsx! { div {} }
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ fn create_random_template_node(
|
||||||
1 => TemplateNode::Text {
|
1 => TemplateNode::Text {
|
||||||
text: Box::leak(format!("{}", rand::random::<usize>()).into_boxed_str()),
|
text: Box::leak(format!("{}", rand::random::<usize>()).into_boxed_str()),
|
||||||
},
|
},
|
||||||
2 => TemplateNode::DynamicText {
|
2 => TemplateNode::Dynamic {
|
||||||
id: {
|
id: {
|
||||||
let old_idx = *template_idx;
|
let old_idx = *template_idx;
|
||||||
*template_idx += 1;
|
*template_idx += 1;
|
||||||
|
@ -118,9 +118,6 @@ fn generate_paths(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TemplateNode::Text { .. } => {}
|
TemplateNode::Text { .. } => {}
|
||||||
TemplateNode::DynamicText { .. } => {
|
|
||||||
node_paths.push(current_path.to_vec());
|
|
||||||
}
|
|
||||||
TemplateNode::Dynamic { .. } => {
|
TemplateNode::Dynamic { .. } => {
|
||||||
node_paths.push(current_path.to_vec());
|
node_paths.push(current_path.to_vec());
|
||||||
}
|
}
|
||||||
|
@ -265,12 +262,11 @@ fn create_random_element(cx: DepthProps) -> Element {
|
||||||
.map(|_| Box::new([create_random_dynamic_attr()]) as Box<[Attribute]>)
|
.map(|_| Box::new([create_random_dynamic_attr()]) as Box<[Attribute]>)
|
||||||
.collect(),
|
.collect(),
|
||||||
);
|
);
|
||||||
Some(node)
|
node
|
||||||
}
|
}
|
||||||
_ => None,
|
_ => VNode::default(),
|
||||||
};
|
};
|
||||||
// println!("{node:#?}");
|
Element::Ok(node)
|
||||||
node
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// test for panics when creating random nodes and templates
|
// test for panics when creating random nodes and templates
|
||||||
|
|
|
@ -37,6 +37,7 @@ fn dual_stream() {
|
||||||
assert_eq!(edits.edits, {
|
assert_eq!(edits.edits, {
|
||||||
[
|
[
|
||||||
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
LoadTemplate { name: "template", index: 0, id: ElementId(1) },
|
||||||
|
HydrateText { path: &[0, 0], value: "123".to_string(), id: ElementId(2) },
|
||||||
SetAttribute {
|
SetAttribute {
|
||||||
name: "class",
|
name: "class",
|
||||||
value: "asd 123 123".into_value(),
|
value: "asd 123 123".into_value(),
|
||||||
|
@ -44,7 +45,6 @@ fn dual_stream() {
|
||||||
ns: None,
|
ns: None,
|
||||||
},
|
},
|
||||||
NewEventListener { name: "click".to_string(), id: ElementId(1) },
|
NewEventListener { name: "click".to_string(), id: ElementId(1) },
|
||||||
HydrateText { path: &[0, 0], value: "123".to_string(), id: ElementId(2) },
|
|
||||||
AppendChildren { id: ElementId(0), m: 1 },
|
AppendChildren { id: ElementId(0), m: 1 },
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
|
@ -52,7 +52,7 @@ fn events_generate() {
|
||||||
"Click me!"
|
"Click me!"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
_ => None,
|
_ => VNode::empty(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -66,7 +66,7 @@ fn events_generate() {
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
let edits = dom.render_immediate_to_vec();
|
let edits = dom.render_immediate_to_vec();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -9,7 +9,7 @@ fn app_drops() {
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ fn hooks_drop() {
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ fn contexts_drop() {
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ fn tasks_drop() {
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -83,7 +83,7 @@ fn root_props_drop() {
|
||||||
);
|
);
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -113,7 +113,7 @@ fn diffing_drops_old() {
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
|
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
|
@ -141,6 +141,6 @@ fn hooks_drop_before_contexts() {
|
||||||
let mut dom = VirtualDom::new(app);
|
let mut dom = VirtualDom::new(app);
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,7 +63,7 @@ fn test_memory_leak() {
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
for _ in 0..5 {
|
for _ in 0..5 {
|
||||||
dom.mark_dirty(ScopeId::ROOT);
|
dom.mark_dirty(ScopeId::APP);
|
||||||
_ = dom.render_immediate_to_vec();
|
_ = dom.render_immediate_to_vec();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ fn memo_works_properly() {
|
||||||
let val = generation();
|
let val = generation();
|
||||||
|
|
||||||
if val == 2 || val == 4 {
|
if val == 2 || val == 4 {
|
||||||
return None;
|
return Element::Ok(VNode::default());
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = use_hook(|| String::from("asd"));
|
let name = use_hook(|| String::from("asd"));
|
||||||
|
@ -123,7 +123,7 @@ fn free_works_on_root_hooks() {
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
// ptr gets cloned into props and then into the hook
|
// ptr gets cloned into props and then into the hook
|
||||||
assert_eq!(Rc::strong_count(&ptr), 5);
|
assert!(Rc::strong_count(&ptr) > 1);
|
||||||
|
|
||||||
drop(dom);
|
drop(dom);
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,22 @@ use dioxus::prelude::*;
|
||||||
use std::future::poll_fn;
|
use std::future::poll_fn;
|
||||||
use std::task::Poll;
|
use std::task::Poll;
|
||||||
|
|
||||||
|
async fn poll_three_times() {
|
||||||
|
// Poll each task 3 times
|
||||||
|
let mut count = 0;
|
||||||
|
poll_fn(|cx| {
|
||||||
|
println!("polling... {}", count);
|
||||||
|
if count < 3 {
|
||||||
|
count += 1;
|
||||||
|
cx.waker().wake_by_ref();
|
||||||
|
Poll::Pending
|
||||||
|
} else {
|
||||||
|
Poll::Ready(())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn suspense_resolves() {
|
fn suspense_resolves() {
|
||||||
// wait just a moment, not enough time for the boundary to resolve
|
// wait just a moment, not enough time for the boundary to resolve
|
||||||
|
@ -15,8 +31,6 @@ fn suspense_resolves() {
|
||||||
let out = dioxus_ssr::render(&dom);
|
let out = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
assert_eq!(out, "<div>Waiting for... child</div>");
|
assert_eq!(out, "<div>Waiting for... child</div>");
|
||||||
|
|
||||||
dbg!(out);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,7 +38,10 @@ fn app() -> Element {
|
||||||
rsx!(
|
rsx!(
|
||||||
div {
|
div {
|
||||||
"Waiting for... "
|
"Waiting for... "
|
||||||
suspended_child {}
|
SuspenseBoundary {
|
||||||
|
fallback: |_| rsx! { "fallback" },
|
||||||
|
suspended_child {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -43,20 +60,7 @@ fn suspended_child() -> Element {
|
||||||
|
|
||||||
if val() < 3 {
|
if val() < 3 {
|
||||||
let task = spawn(async move {
|
let task = spawn(async move {
|
||||||
// Poll each task 3 times
|
poll_three_times().await;
|
||||||
let mut count = 0;
|
|
||||||
poll_fn(|cx| {
|
|
||||||
println!("polling... {}", count);
|
|
||||||
if count < 3 {
|
|
||||||
count += 1;
|
|
||||||
cx.waker().wake_by_ref();
|
|
||||||
Poll::Pending
|
|
||||||
} else {
|
|
||||||
Poll::Ready(())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
println!("waiting... {}", val);
|
println!("waiting... {}", val);
|
||||||
val += 1;
|
val += 1;
|
||||||
});
|
});
|
||||||
|
@ -65,3 +69,306 @@ fn suspended_child() -> Element {
|
||||||
|
|
||||||
rsx!("child")
|
rsx!("child")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// When switching from a suspense fallback to the real child, the state of that component must be kept
|
||||||
|
#[test]
|
||||||
|
fn suspense_keeps_state() {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
dom.render_suspense_immediate().await;
|
||||||
|
|
||||||
|
let out = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
|
assert_eq!(out, "fallback");
|
||||||
|
|
||||||
|
dom.wait_for_suspense().await;
|
||||||
|
let out = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
|
assert_eq!(out, "<div>child with future resolved</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
rsx! {
|
||||||
|
SuspenseBoundary {
|
||||||
|
fallback: |_| rsx! { "fallback" },
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Child() -> Element {
|
||||||
|
let mut future_resolved = use_signal(|| false);
|
||||||
|
|
||||||
|
let task = use_hook(|| {
|
||||||
|
spawn(async move {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
future_resolved.set(true);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !future_resolved() {
|
||||||
|
suspend(task)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("future resolved: {future_resolved:?}");
|
||||||
|
|
||||||
|
if future_resolved() {
|
||||||
|
rsx! {
|
||||||
|
div { "child with future resolved" }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rsx! {
|
||||||
|
div { "this should never be rendered" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// spawn doesn't run in suspense
|
||||||
|
#[test]
|
||||||
|
fn suspense_does_not_poll_spawn() {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
|
dom.wait_for_suspense().await;
|
||||||
|
let out = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
|
assert_eq!(out, "<div>child with future resolved</div>");
|
||||||
|
});
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
rsx! {
|
||||||
|
SuspenseBoundary {
|
||||||
|
fallback: |_| rsx! { "fallback" },
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Child() -> Element {
|
||||||
|
let mut future_resolved = use_signal(|| false);
|
||||||
|
|
||||||
|
// futures that are spawned, but not suspended should never be polled
|
||||||
|
use_hook(|| {
|
||||||
|
spawn(async move {
|
||||||
|
panic!("Non-suspended task was polled");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let task = use_hook(|| {
|
||||||
|
spawn(async move {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
future_resolved.set(true);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !future_resolved() {
|
||||||
|
suspend(task)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div { "child with future resolved" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// suspended nodes are not mounted, so they should not run effects
|
||||||
|
#[test]
|
||||||
|
fn suspended_nodes_dont_trigger_effects() {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
|
let work = async move {
|
||||||
|
loop {
|
||||||
|
dom.wait_for_work().await;
|
||||||
|
dom.render_immediate(&mut dioxus_core::NoOpMutations);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
tokio::select! {
|
||||||
|
_ = work => {},
|
||||||
|
_ = tokio::time::sleep(std::time::Duration::from_millis(100)) => {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
rsx! {
|
||||||
|
SuspenseBoundary {
|
||||||
|
fallback: |_| rsx! { "fallback" },
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn RerendersFrequently() -> Element {
|
||||||
|
let mut count = use_signal(|| 0);
|
||||||
|
|
||||||
|
use_future(move || async move {
|
||||||
|
for _ in 0..100 {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(10)).await;
|
||||||
|
count.set(count() + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div { "rerenders frequently" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Child() -> Element {
|
||||||
|
let mut future_resolved = use_signal(|| false);
|
||||||
|
|
||||||
|
use_effect(|| panic!("effects should not run during suspense"));
|
||||||
|
|
||||||
|
let task = use_hook(|| {
|
||||||
|
spawn(async move {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
||||||
|
future_resolved.set(true);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if !future_resolved() {
|
||||||
|
suspend(task)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div { "child with future resolved" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure we keep any state of components when we switch from a resolved future to a suspended future
|
||||||
|
#[test]
|
||||||
|
fn resolved_to_suspended() {
|
||||||
|
tracing_subscriber::fmt::SubscriberBuilder::default()
|
||||||
|
.with_max_level(tracing::Level::INFO)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
static SUSPENDED: GlobalSignal<bool> = Signal::global(|| false);
|
||||||
|
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
|
let out = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
|
assert_eq!(out, "rendered 1 times");
|
||||||
|
|
||||||
|
dom.in_runtime(|| ScopeId::APP.in_runtime(|| *SUSPENDED.write() = true));
|
||||||
|
|
||||||
|
dom.render_suspense_immediate().await;
|
||||||
|
let out = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
|
assert_eq!(out, "fallback");
|
||||||
|
|
||||||
|
dom.wait_for_suspense().await;
|
||||||
|
let out = dioxus_ssr::render(&dom);
|
||||||
|
|
||||||
|
assert_eq!(out, "rendered 3 times");
|
||||||
|
});
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
rsx! {
|
||||||
|
SuspenseBoundary {
|
||||||
|
fallback: |_| rsx! { "fallback" },
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Child() -> Element {
|
||||||
|
let mut render_count = use_signal(|| 0);
|
||||||
|
render_count += 1;
|
||||||
|
|
||||||
|
let mut task = use_hook(|| CopyValue::new(None));
|
||||||
|
|
||||||
|
tracing::info!("render_count: {}", render_count.peek());
|
||||||
|
|
||||||
|
if SUSPENDED() {
|
||||||
|
if task().is_none() {
|
||||||
|
task.set(Some(spawn(async move {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
tracing::info!("task finished");
|
||||||
|
*SUSPENDED.write() = false;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
suspend(task().unwrap())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
"rendered {render_count.peek()} times"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Make sure suspense tells the renderer that a suspense boundary was resolved
|
||||||
|
#[test]
|
||||||
|
fn suspense_tracks_resolved() {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_time()
|
||||||
|
.build()
|
||||||
|
.unwrap()
|
||||||
|
.block_on(async {
|
||||||
|
let mut dom = VirtualDom::new(app);
|
||||||
|
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
||||||
|
|
||||||
|
dom.render_suspense_immediate().await;
|
||||||
|
dom.wait_for_suspense_work().await;
|
||||||
|
assert_eq!(
|
||||||
|
dom.render_suspense_immediate().await,
|
||||||
|
vec![ScopeId(ScopeId::APP.0 + 1)]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
fn app() -> Element {
|
||||||
|
rsx! {
|
||||||
|
SuspenseBoundary {
|
||||||
|
fallback: |_| rsx! { "fallback" },
|
||||||
|
Child {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Child() -> Element {
|
||||||
|
let mut resolved = use_signal(|| false);
|
||||||
|
let task = use_hook(|| {
|
||||||
|
spawn(async move {
|
||||||
|
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
|
||||||
|
tracing::info!("task finished");
|
||||||
|
resolved.set(true);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if resolved() {
|
||||||
|
println!("suspense is resolved");
|
||||||
|
} else {
|
||||||
|
println!("suspense is not resolved");
|
||||||
|
suspend(task)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
"child"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -126,95 +126,3 @@ async fn yield_now_works() {
|
||||||
|
|
||||||
SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20));
|
SEQUENCE.with(|s| assert_eq!(s.borrow().len(), 20));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure that calling wait_for_flush waits for dioxus to finish its synchronous work
|
|
||||||
#[tokio::test]
|
|
||||||
async fn flushing() {
|
|
||||||
thread_local! {
|
|
||||||
static SEQUENCE: std::cell::RefCell<Vec<usize>> = const { std::cell::RefCell::new(Vec::new()) };
|
|
||||||
static BROADCAST: (tokio::sync::broadcast::Sender<()>, tokio::sync::broadcast::Receiver<()>) = tokio::sync::broadcast::channel(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn app() -> Element {
|
|
||||||
if generation() > 0 {
|
|
||||||
println!("App");
|
|
||||||
SEQUENCE.with(|s| s.borrow_mut().push(0));
|
|
||||||
}
|
|
||||||
|
|
||||||
// The next two tasks mimic effects. They should only be run after the app has been rendered
|
|
||||||
use_hook(|| {
|
|
||||||
spawn(async move {
|
|
||||||
let mut channel = BROADCAST.with(|b| b.1.resubscribe());
|
|
||||||
for _ in 0..10 {
|
|
||||||
wait_for_next_render().await;
|
|
||||||
println!("Task 1 recved");
|
|
||||||
channel.recv().await.unwrap();
|
|
||||||
println!("Task 1");
|
|
||||||
SEQUENCE.with(|s| s.borrow_mut().push(1));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
use_hook(|| {
|
|
||||||
spawn(async move {
|
|
||||||
let mut channel = BROADCAST.with(|b| b.1.resubscribe());
|
|
||||||
for _ in 0..10 {
|
|
||||||
wait_for_next_render().await;
|
|
||||||
println!("Task 2 recved");
|
|
||||||
channel.recv().await.unwrap();
|
|
||||||
println!("Task 2");
|
|
||||||
SEQUENCE.with(|s| s.borrow_mut().push(2));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
rsx! {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dom = VirtualDom::new(app);
|
|
||||||
|
|
||||||
dom.rebuild(&mut dioxus_core::NoOpMutations);
|
|
||||||
|
|
||||||
let fut = async {
|
|
||||||
// Trigger the flush by waiting for work
|
|
||||||
for i in 0..10 {
|
|
||||||
BROADCAST.with(|b| b.0.send(()).unwrap());
|
|
||||||
dom.mark_dirty(ScopeId(0));
|
|
||||||
dom.wait_for_work().await;
|
|
||||||
dom.render_immediate(&mut dioxus_core::NoOpMutations);
|
|
||||||
println!("Flushed {}", i);
|
|
||||||
}
|
|
||||||
BROADCAST.with(|b| b.0.send(()).unwrap());
|
|
||||||
dom.wait_for_work().await;
|
|
||||||
};
|
|
||||||
|
|
||||||
tokio::select! {
|
|
||||||
_ = fut => {}
|
|
||||||
_ = tokio::time::sleep(Duration::from_millis(500)) => {
|
|
||||||
println!("Aborting due to timeout");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
SEQUENCE.with(|s| {
|
|
||||||
let s = s.borrow();
|
|
||||||
println!("{:?}", s);
|
|
||||||
assert_eq!(s.len(), 30);
|
|
||||||
// We need to check if every three elements look like [0, 1, 2] or [0, 2, 1]
|
|
||||||
let mut has_seen_1 = false;
|
|
||||||
for (i, &x) in s.iter().enumerate() {
|
|
||||||
let stage = i % 3;
|
|
||||||
if stage == 0 {
|
|
||||||
assert_eq!(x, 0);
|
|
||||||
} else if stage == 1 {
|
|
||||||
assert!(x == 1 || x == 2);
|
|
||||||
has_seen_1 = x == 1;
|
|
||||||
} else if stage == 2 {
|
|
||||||
if has_seen_1 {
|
|
||||||
assert_eq!(x, 2);
|
|
||||||
} else {
|
|
||||||
assert_eq!(x, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -155,8 +155,8 @@ path = "../../examples/dynamic_asset.rs"
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "error_handle"
|
name = "errors"
|
||||||
path = "../../examples/error_handle.rs"
|
path = "../../examples/errors.rs"
|
||||||
doc-scrape-examples = true
|
doc-scrape-examples = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
|
|
|
@ -61,5 +61,5 @@ fn app() -> Element {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
None
|
VNode::empty()
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,16 +12,16 @@ rust-version = "1.65.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-html = { workspace = true, optional = true }
|
dioxus-html = { workspace = true, default-features = false, optional = true }
|
||||||
dioxus-core-macro = { workspace = true, optional = true }
|
dioxus-core-macro = { workspace = true, optional = true }
|
||||||
dioxus-config-macro = { workspace = true, optional = true }
|
dioxus-config-macro = { workspace = true, optional = true }
|
||||||
dioxus-hooks = { workspace = true, optional = true }
|
dioxus-hooks = { workspace = true, optional = true }
|
||||||
dioxus-signals = { workspace = true, optional = true }
|
dioxus-signals = { workspace = true, optional = true }
|
||||||
dioxus-router = { workspace = true, optional = true }
|
dioxus-router = { workspace = true, optional = true }
|
||||||
dioxus-web = { workspace = true, optional = true }
|
dioxus-web = { workspace = true, default-features = false, optional = true }
|
||||||
dioxus-mobile = { workspace = true, optional = true }
|
dioxus-mobile = { workspace = true, optional = true }
|
||||||
dioxus-desktop = { workspace = true, default-features = true, optional = true }
|
dioxus-desktop = { workspace = true, default-features = true, optional = true }
|
||||||
dioxus-fullstack = { workspace = true, optional = true }
|
dioxus-fullstack = { workspace = true, default-features = true, optional = true }
|
||||||
dioxus-static-site-generation = { workspace = true, optional = true }
|
dioxus-static-site-generation = { workspace = true, optional = true }
|
||||||
dioxus-liveview = { workspace = true, optional = true }
|
dioxus-liveview = { workspace = true, optional = true }
|
||||||
dioxus-ssr ={ workspace = true, optional = true }
|
dioxus-ssr ={ workspace = true, optional = true }
|
||||||
|
@ -33,12 +33,16 @@ axum = { workspace = true, optional = true }
|
||||||
dioxus-hot-reload = { workspace = true, optional = true }
|
dioxus-hot-reload = { workspace = true, optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["macro", "html", "hot-reload", "signals", "hooks", "launch"]
|
default = ["macro", "html", "hot-reload", "signals", "hooks", "launch", "mounted", "file_engine", "eval"]
|
||||||
|
minimal = ["macro", "html", "signals", "hooks", "launch"]
|
||||||
signals = ["dep:dioxus-signals"]
|
signals = ["dep:dioxus-signals"]
|
||||||
macro = ["dep:dioxus-core-macro"]
|
macro = ["dep:dioxus-core-macro"]
|
||||||
html = ["dep:dioxus-html"]
|
html = ["dep:dioxus-html"]
|
||||||
hooks = ["dep:dioxus-hooks"]
|
hooks = ["dep:dioxus-hooks"]
|
||||||
hot-reload = ["dep:dioxus-hot-reload"]
|
hot-reload = ["dep:dioxus-hot-reload", "dioxus-web?/hot_reload", "dioxus-fullstack?/hot-reload"]
|
||||||
|
mounted = ["dioxus-web?/mounted", "dioxus-html?/mounted"]
|
||||||
|
file_engine = ["dioxus-web?/file_engine"]
|
||||||
|
eval = ["dioxus-web?/eval", "dioxus-html?/eval"]
|
||||||
|
|
||||||
launch = ["dep:dioxus-config-macro"]
|
launch = ["dep:dioxus-config-macro"]
|
||||||
router = ["dep:dioxus-router"]
|
router = ["dep:dioxus-router"]
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
pub use dioxus_core;
|
pub use dioxus_core;
|
||||||
|
pub use dioxus_core::{CapturedError, Ok, Result};
|
||||||
|
|
||||||
#[cfg(feature = "launch")]
|
#[cfg(feature = "launch")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "launch")))]
|
||||||
|
|
|
@ -27,7 +27,8 @@ hyper = { workspace = true, optional = true }
|
||||||
http = { workspace = true, optional = true }
|
http = { workspace = true, optional = true }
|
||||||
|
|
||||||
# Web Integration
|
# Web Integration
|
||||||
dioxus-web = { workspace = true, features = ["hydrate"], optional = true }
|
dioxus-web = { workspace = true, features = ["hydrate"], default-features = false, optional = true }
|
||||||
|
dioxus-interpreter-js = { workspace = true, optional = true }
|
||||||
|
|
||||||
# Desktop Integration
|
# Desktop Integration
|
||||||
dioxus-desktop = { workspace = true, optional = true }
|
dioxus-desktop = { workspace = true, optional = true }
|
||||||
|
@ -37,22 +38,23 @@ dioxus-mobile = { workspace = true, optional = true }
|
||||||
|
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
tracing-futures = { workspace = true, optional = true }
|
tracing-futures = { workspace = true, optional = true }
|
||||||
once_cell = "1.17.1"
|
once_cell = { workspace = true }
|
||||||
tokio-util = { version = "0.7.8", features = ["rt"], optional = true }
|
tokio-util = { version = "0.7.8", features = ["rt"], optional = true }
|
||||||
|
async-trait = { version = "0.1.58", optional = true }
|
||||||
|
|
||||||
serde = "1.0.159"
|
serde = "1.0.159"
|
||||||
serde_json = { version = "1.0.95", optional = true }
|
|
||||||
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
|
tokio-stream = { version = "0.1.12", features = ["sync"], optional = true }
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
ciborium = "0.2.1"
|
futures-channel = { workspace = true }
|
||||||
base64 = "0.21.0"
|
ciborium = { workspace = true }
|
||||||
|
base64 = { workspace = true }
|
||||||
|
|
||||||
pin-project = { version = "1.1.2", optional = true }
|
pin-project = { version = "1.1.2", optional = true }
|
||||||
thiserror = { workspace = true, optional = true }
|
thiserror = { workspace = true, optional = true }
|
||||||
async-trait = "0.1.71"
|
|
||||||
bytes = "1.4.0"
|
bytes = "1.4.0"
|
||||||
tower = { workspace = true, features = ["util"], optional = true }
|
tower = { workspace = true, features = ["util"], optional = true }
|
||||||
tower-layer = { version = "0.3.2", optional = true }
|
tower-layer = { version = "0.3.2", optional = true }
|
||||||
|
parking_lot = { version = "0.12.1", features = ["send_guard"], optional = true }
|
||||||
web-sys = { version = "0.3.61", optional = true, features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
|
web-sys = { version = "0.3.61", optional = true, features = ["Window", "Document", "Element", "HtmlDocument", "Storage", "console"] }
|
||||||
|
|
||||||
dioxus-cli-config = { workspace = true, features = ["read-config"], optional = true }
|
dioxus-cli-config = { workspace = true, features = ["read-config"], optional = true }
|
||||||
|
@ -69,14 +71,18 @@ tokio = { workspace = true, features = ["rt", "sync", "rt-multi-thread"], option
|
||||||
dioxus = { workspace = true, features = ["fullstack"] }
|
dioxus = { workspace = true, features = ["fullstack"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["hot-reload"]
|
default = ["hot-reload", "panic_hook"]
|
||||||
hot-reload = ["dep:serde_json", "dioxus-hot-reload/serve"]
|
panic_hook = ["dioxus-web?/panic_hook"]
|
||||||
|
hot-reload = ["dioxus-web?/hot_reload", "dioxus-hot-reload/serve"]
|
||||||
|
mounted = ["dioxus-web?/mounted"]
|
||||||
|
file_engine = ["dioxus-web?/file_engine"]
|
||||||
|
eval = ["dioxus-web?/eval"]
|
||||||
web = ["dep:dioxus-web", "dep:web-sys"]
|
web = ["dep:dioxus-web", "dep:web-sys"]
|
||||||
desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
|
desktop = ["dep:dioxus-desktop", "server_fn/reqwest", "dioxus_server_macro/reqwest"]
|
||||||
mobile = ["dep:dioxus-mobile"]
|
mobile = ["dep:dioxus-mobile"]
|
||||||
default-tls = ["server_fn/default-tls"]
|
default-tls = ["server_fn/default-tls"]
|
||||||
rustls = ["server_fn/rustls"]
|
rustls = ["server_fn/rustls"]
|
||||||
axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum"]
|
axum = ["dep:axum", "dep:tower-http", "server", "server_fn/axum", "dioxus_server_macro/axum", "default-tls"]
|
||||||
static-site-generation = []
|
static-site-generation = []
|
||||||
server = [
|
server = [
|
||||||
"server_fn/ssr",
|
"server_fn/ssr",
|
||||||
|
@ -94,6 +100,9 @@ server = [
|
||||||
"dep:pin-project",
|
"dep:pin-project",
|
||||||
"dep:thiserror",
|
"dep:thiserror",
|
||||||
"dep:dioxus-cli-config",
|
"dep:dioxus-cli-config",
|
||||||
|
"dep:async-trait",
|
||||||
|
"dep:parking_lot",
|
||||||
|
"dioxus-interpreter-js",
|
||||||
"dep:clap",
|
"dep:clap",
|
||||||
"dioxus-cli-config/cli"
|
"dioxus-cli-config/cli"
|
||||||
]
|
]
|
||||||
|
|
|
@ -50,7 +50,6 @@ fn main() {
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
// Server side render the application, serve static assets, and register server functions
|
// Server side render the application, serve static assets, and register server functions
|
||||||
.serve_dioxus_application(ServeConfig::default(), app)
|
.serve_dioxus_application(ServeConfig::default(), app)
|
||||||
.await
|
|
||||||
.layer(
|
.layer(
|
||||||
axum_session_auth::AuthSessionLayer::<
|
axum_session_auth::AuthSessionLayer::<
|
||||||
crate::auth::User,
|
crate::auth::User,
|
||||||
|
|
|
@ -28,7 +28,6 @@ fn app() -> Element {
|
||||||
"Run a server function!"
|
"Run a server function!"
|
||||||
}
|
}
|
||||||
"Server said: {text}"
|
"Server said: {text}"
|
||||||
"{server_future.state():?}"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
2
packages/fullstack/examples/hackernews/.gitignore
vendored
Normal file
2
packages/fullstack/examples/hackernews/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/static
|
||||||
|
/dist
|
21
packages/fullstack/examples/hackernews/Cargo.toml
Normal file
21
packages/fullstack/examples/hackernews/Cargo.toml
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
[package]
|
||||||
|
name = "dioxus-hackernews"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Evan Almloff <evanalmloff@gmail.com>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
dioxus = { workspace = true, features = ["fullstack", "router"] }
|
||||||
|
chrono = { version = "0.4.38", features = ["serde"] }
|
||||||
|
reqwest = { version = "0.12.4", features = ["json"] }
|
||||||
|
serde = { version = "1.0.203", features = ["derive"] }
|
||||||
|
tracing-wasm = "0.2.1"
|
||||||
|
tracing = { workspace = true }
|
||||||
|
tracing-subscriber = "0.3.17"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
server = ["dioxus/axum"]
|
||||||
|
web = ["dioxus/web"]
|
326
packages/fullstack/examples/hackernews/src/main.rs
Normal file
326
packages/fullstack/examples/hackernews/src/main.rs
Normal file
|
@ -0,0 +1,326 @@
|
||||||
|
#![allow(non_snake_case, unused)]
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
// Define the Hackernews API and types
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Formatter},
|
||||||
|
num::ParseIntError,
|
||||||
|
str::FromStr,
|
||||||
|
};
|
||||||
|
use svg_attributes::to;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
tracing_wasm::set_as_global_default();
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
|
launch(|| rsx! { Router::<Route> {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Routable)]
|
||||||
|
enum Route {
|
||||||
|
#[redirect("/", || Route::Homepage { story: PreviewState { active_story: None } })]
|
||||||
|
#[route("/:story")]
|
||||||
|
Homepage { story: PreviewState },
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn App() -> Element {
|
||||||
|
rsx! {
|
||||||
|
Router::<Route> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const STYLE: &str = r#"@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.spinner {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
}"#;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Homepage(story: ReadOnlySignal<PreviewState>) -> Element {
|
||||||
|
rsx! {
|
||||||
|
style {
|
||||||
|
{STYLE}
|
||||||
|
}
|
||||||
|
div { display: "flex", flex_direction: "row", width: "100%",
|
||||||
|
div {
|
||||||
|
width: "50%",
|
||||||
|
SuspenseBoundary {
|
||||||
|
fallback: |context: SuspenseContext| rsx! {
|
||||||
|
"Loading..."
|
||||||
|
},
|
||||||
|
Stories {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { width: "50%",
|
||||||
|
SuspenseBoundary {
|
||||||
|
fallback: |context: SuspenseContext| rsx! {
|
||||||
|
"Loading preview..."
|
||||||
|
},
|
||||||
|
Preview {
|
||||||
|
story
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Stories() -> Element {
|
||||||
|
let stories: Resource<dioxus::Result<Vec<i64>>> = use_server_future(move || async move {
|
||||||
|
let url = format!("{}topstories.json", BASE_API_URL);
|
||||||
|
let mut stories_ids = reqwest::get(&url).await?.json::<Vec<i64>>().await?;
|
||||||
|
stories_ids.truncate(30);
|
||||||
|
Ok(stories_ids)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match stories().unwrap() {
|
||||||
|
Ok(list) => rsx! {
|
||||||
|
div {
|
||||||
|
for story in list {
|
||||||
|
ChildrenOrLoading {
|
||||||
|
key: "{story}",
|
||||||
|
StoryListing { story }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => rsx! {"An error occurred while fetching stories {err}"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn StoryListing(story: ReadOnlySignal<i64>) -> Element {
|
||||||
|
let story = use_server_future(move || get_story(story()))?;
|
||||||
|
|
||||||
|
let StoryItem {
|
||||||
|
title,
|
||||||
|
url,
|
||||||
|
by,
|
||||||
|
score,
|
||||||
|
time,
|
||||||
|
kids,
|
||||||
|
id,
|
||||||
|
..
|
||||||
|
} = story().unwrap()?.item;
|
||||||
|
|
||||||
|
let url = url.as_deref().unwrap_or_default();
|
||||||
|
let hostname = url
|
||||||
|
.trim_start_matches("https://")
|
||||||
|
.trim_start_matches("http://")
|
||||||
|
.trim_start_matches("www.");
|
||||||
|
let score = format!("{score} {}", if score == 1 { " point" } else { " points" });
|
||||||
|
let comments = format!(
|
||||||
|
"{} {}",
|
||||||
|
kids.len(),
|
||||||
|
if kids.len() == 1 {
|
||||||
|
" comment"
|
||||||
|
} else {
|
||||||
|
" comments"
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let time = time.format("%D %l:%M %p");
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
padding: "0.5rem",
|
||||||
|
position: "relative",
|
||||||
|
div { font_size: "1.5rem",
|
||||||
|
Link {
|
||||||
|
to: Route::Homepage { story: PreviewState { active_story: Some(id) } },
|
||||||
|
"{title}"
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: "gray",
|
||||||
|
href: "https://news.ycombinator.com/from?site={hostname}",
|
||||||
|
text_decoration: "none",
|
||||||
|
" ({hostname})"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
div { display: "flex", flex_direction: "row", color: "gray",
|
||||||
|
div { "{score}" }
|
||||||
|
div { padding_left: "0.5rem", "by {by}" }
|
||||||
|
div { padding_left: "0.5rem", "{time}" }
|
||||||
|
div { padding_left: "0.5rem", "{comments}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct PreviewState {
|
||||||
|
active_story: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for PreviewState {
|
||||||
|
type Err = ParseIntError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let state = i64::from_str(s)?;
|
||||||
|
Ok(PreviewState {
|
||||||
|
active_story: Some(state),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for PreviewState {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(id) = &self.active_story {
|
||||||
|
write!(f, "{id}")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Preview(story: ReadOnlySignal<PreviewState>) -> Element {
|
||||||
|
let PreviewState {
|
||||||
|
active_story: Some(id),
|
||||||
|
} = story()
|
||||||
|
else {
|
||||||
|
return rsx! {"Hover over a story to preview it here"};
|
||||||
|
};
|
||||||
|
|
||||||
|
let story = use_server_future(use_reactive!(|id| get_story(id)))?;
|
||||||
|
|
||||||
|
let story = story().unwrap()?;
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div { padding: "0.5rem",
|
||||||
|
div { font_size: "1.5rem", a { href: story.item.url, "{story.item.title}" } }
|
||||||
|
if let Some(text) = &story.item.text { div { dangerous_inner_html: "{text}" } }
|
||||||
|
for comment in story.item.kids.iter().copied() {
|
||||||
|
ChildrenOrLoading {
|
||||||
|
key: "{comment}",
|
||||||
|
Comment { comment }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn Comment(comment: i64) -> Element {
|
||||||
|
let comment: Resource<dioxus::Result<CommentData>> =
|
||||||
|
use_server_future(use_reactive!(|comment| async move {
|
||||||
|
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, comment);
|
||||||
|
let mut comment = reqwest::get(&url).await?.json::<CommentData>().await?;
|
||||||
|
Ok(comment)
|
||||||
|
}))?;
|
||||||
|
|
||||||
|
let CommentData {
|
||||||
|
by,
|
||||||
|
time,
|
||||||
|
text,
|
||||||
|
id,
|
||||||
|
kids,
|
||||||
|
..
|
||||||
|
} = comment().unwrap()?;
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
div { padding: "0.5rem",
|
||||||
|
div { color: "gray", "by {by}" }
|
||||||
|
div { dangerous_inner_html: "{text}" }
|
||||||
|
for comment in kids.iter().copied() {
|
||||||
|
ChildrenOrLoading {
|
||||||
|
key: "{comment}",
|
||||||
|
Comment { comment }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub static BASE_API_URL: &str = "https://hacker-news.firebaseio.com/v0/";
|
||||||
|
pub static ITEM_API: &str = "item/";
|
||||||
|
pub static USER_API: &str = "user/";
|
||||||
|
const COMMENT_DEPTH: i64 = 1;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct StoryPageData {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub item: StoryItem,
|
||||||
|
#[serde(default)]
|
||||||
|
pub comments: Vec<CommentData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct CommentData {
|
||||||
|
pub id: i64,
|
||||||
|
/// there will be no by field if the comment was deleted
|
||||||
|
#[serde(default)]
|
||||||
|
pub by: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub text: String,
|
||||||
|
#[serde(with = "chrono::serde::ts_seconds")]
|
||||||
|
pub time: DateTime<Utc>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub kids: Vec<i64>,
|
||||||
|
pub r#type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct StoryItem {
|
||||||
|
pub id: i64,
|
||||||
|
pub title: String,
|
||||||
|
pub url: Option<String>,
|
||||||
|
pub text: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub by: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub score: i64,
|
||||||
|
#[serde(default)]
|
||||||
|
pub descendants: i64,
|
||||||
|
#[serde(with = "chrono::serde::ts_seconds")]
|
||||||
|
pub time: DateTime<Utc>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub kids: Vec<i64>,
|
||||||
|
pub r#type: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_story(id: i64) -> dioxus::Result<StoryPageData> {
|
||||||
|
let url = format!("{}{}{}.json", BASE_API_URL, ITEM_API, id);
|
||||||
|
Ok(reqwest::get(&url).await?.json::<StoryPageData>().await?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn ChildrenOrLoading(children: Element) -> Element {
|
||||||
|
rsx! {
|
||||||
|
SuspenseBoundary {
|
||||||
|
fallback: |context: SuspenseContext| {
|
||||||
|
rsx! {
|
||||||
|
if let Some(placeholder) = context.suspense_placeholder() {
|
||||||
|
{placeholder}
|
||||||
|
} else {
|
||||||
|
LoadingIndicator {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn LoadingIndicator() -> Element {
|
||||||
|
rsx! {
|
||||||
|
div {
|
||||||
|
class: "spinner",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +1,14 @@
|
||||||
//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
|
//! Dioxus utilities for the [Axum](https://docs.rs/axum/latest/axum/index.html) server framework.
|
||||||
//!
|
//!
|
||||||
//! # Example
|
//! # Example
|
||||||
//! ```rust
|
//! ```rust, no_run
|
||||||
//! #![allow(non_snake_case)]
|
//! #![allow(non_snake_case)]
|
||||||
//! use dioxus_lib::prelude::*;
|
//! use dioxus::prelude::*;
|
||||||
//! use dioxus_fullstack::prelude::*;
|
|
||||||
//!
|
//!
|
||||||
//! fn main() {
|
//! fn main() {
|
||||||
//! #[cfg(feature = "web")]
|
//! #[cfg(feature = "web")]
|
||||||
//! // Hydrate the application on the client
|
//! // Hydrate the application on the client
|
||||||
//! dioxus_web::launch_cfg(app, dioxus_web::Config::new().hydrate(true));
|
//! launch(app);
|
||||||
//! #[cfg(feature = "server")]
|
//! #[cfg(feature = "server")]
|
||||||
//! {
|
//! {
|
||||||
//! tokio::runtime::Runtime::new()
|
//! tokio::runtime::Runtime::new()
|
||||||
|
@ -22,7 +21,7 @@
|
||||||
//! listener,
|
//! listener,
|
||||||
//! axum::Router::new()
|
//! axum::Router::new()
|
||||||
//! // Server side render the application, serve static assets, and register server functions
|
//! // Server side render the application, serve static assets, and register server functions
|
||||||
//! .serve_dioxus_application(ServerConfig::new(), app)
|
//! .serve_dioxus_application(ServeConfig::default(), app)
|
||||||
//! .into_make_service(),
|
//! .into_make_service(),
|
||||||
//! )
|
//! )
|
||||||
//! .await
|
//! .await
|
||||||
|
@ -44,7 +43,7 @@
|
||||||
//! "Run a server function"
|
//! "Run a server function"
|
||||||
//! }
|
//! }
|
||||||
//! "Server said: {text}"
|
//! "Server said: {text}"
|
||||||
//! })
|
//! }
|
||||||
//! }
|
//! }
|
||||||
//!
|
//!
|
||||||
//! #[server(GetServerData)]
|
//! #[server(GetServerData)]
|
||||||
|
@ -61,7 +60,6 @@ use axum::{
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
};
|
};
|
||||||
use dioxus_lib::prelude::{Element, VirtualDom};
|
use dioxus_lib::prelude::{Element, VirtualDom};
|
||||||
use futures_util::Future;
|
|
||||||
use http::header::*;
|
use http::header::*;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -74,21 +72,18 @@ pub trait DioxusRouterExt<S> {
|
||||||
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
|
/// Registers server functions with the default handler. This handler function will pass an empty [`DioxusServerContext`] to your server functions.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```rust
|
/// ```rust, no_run
|
||||||
/// # use dioxus_lib::prelude::*;
|
/// # use dioxus_lib::prelude::*;
|
||||||
/// # use dioxus_fullstack::prelude::*;
|
/// # use dioxus_fullstack::prelude::*;
|
||||||
/// #[tokio::main]
|
/// #[tokio::main]
|
||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
/// axum::Server::bind(&addr)
|
/// let router = axum::Router::new()
|
||||||
/// .serve(
|
/// // Register server functions routes with the default handler
|
||||||
/// axum::Router::new()
|
/// .register_server_functions()
|
||||||
/// // Register server functions routes with the default handler
|
/// .into_make_service();
|
||||||
/// .register_server_functions()
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
/// .into_make_service(),
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
/// )
|
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
fn register_server_functions(self) -> Self
|
fn register_server_functions(self) -> Self
|
||||||
|
@ -101,21 +96,19 @@ pub trait DioxusRouterExt<S> {
|
||||||
/// Registers server functions with some additional context to insert into the [`DioxusServerContext`] for that handler.
|
/// Registers server functions with some additional context to insert into the [`DioxusServerContext`] for that handler.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```rust
|
/// ```rust, no_run
|
||||||
/// # use dioxus_lib::prelude::*;
|
/// # use dioxus_lib::prelude::*;
|
||||||
/// # use dioxus_fullstack::prelude::*;
|
/// # use dioxus_fullstack::prelude::*;
|
||||||
|
/// # use std::sync::Arc;
|
||||||
/// #[tokio::main]
|
/// #[tokio::main]
|
||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
/// axum::Server::bind(&addr)
|
/// let router = axum::Router::new()
|
||||||
/// .serve(
|
/// // Register server functions routes with the default handler
|
||||||
/// axum::Router::new()
|
/// .register_server_functions_with_context(Arc::new(vec![Box::new(|| Box::new(1234567890u32))]))
|
||||||
/// // Register server functions routes with the default handler
|
/// .into_make_service();
|
||||||
/// .register_server_functions_with_context(vec![Box::new(|| 1234567890u32)])
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
/// .into_make_service(),
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
/// )
|
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self;
|
fn register_server_functions_with_context(self, context_providers: ContextProviders) -> Self;
|
||||||
|
@ -123,34 +116,24 @@ pub trait DioxusRouterExt<S> {
|
||||||
/// Serves the static WASM for your Dioxus application (except the generated index.html).
|
/// Serves the static WASM for your Dioxus application (except the generated index.html).
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```rust
|
/// ```rust, no_run
|
||||||
/// # #![allow(non_snake_case)]
|
/// # #![allow(non_snake_case)]
|
||||||
/// # use dioxus_lib::prelude::*;
|
/// # use dioxus_lib::prelude::*;
|
||||||
/// # use dioxus_fullstack::prelude::*;
|
/// # use dioxus_fullstack::prelude::*;
|
||||||
/// #[tokio::main]
|
/// #[tokio::main]
|
||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
/// axum::Server::bind(&addr)
|
/// let router = axum::Router::new()
|
||||||
/// .serve(
|
/// // Server side render the application, serve static assets, and register server functions
|
||||||
/// axum::Router::new()
|
/// .serve_static_assets("dist")
|
||||||
/// // Server side render the application, serve static assets, and register server functions
|
/// // Server render the application
|
||||||
/// .serve_static_assets("dist")
|
/// // ...
|
||||||
/// // Server render the application
|
/// .into_make_service();
|
||||||
/// // ...
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
/// .into_make_service(),
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
/// )
|
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// fn app() -> Element {
|
|
||||||
/// unimplemented!()
|
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
fn serve_static_assets(
|
fn serve_static_assets(self, assets_path: impl Into<std::path::PathBuf>) -> Self
|
||||||
self,
|
|
||||||
assets_path: impl Into<std::path::PathBuf>,
|
|
||||||
) -> impl Future<Output = Self> + Send + Sync
|
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
|
|
||||||
|
@ -158,33 +141,26 @@ pub trait DioxusRouterExt<S> {
|
||||||
/// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
|
/// This will serve static assets, server render the application, register server functions, and integrate with hot reloading.
|
||||||
///
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```rust
|
/// ```rust, no_run
|
||||||
/// # #![allow(non_snake_case)]
|
/// # #![allow(non_snake_case)]
|
||||||
/// # use dioxus_lib::prelude::*;
|
/// # use dioxus_lib::prelude::*;
|
||||||
/// # use dioxus_fullstack::prelude::*;
|
/// # use dioxus_fullstack::prelude::*;
|
||||||
/// #[tokio::main]
|
/// #[tokio::main]
|
||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
/// axum::Server::bind(&addr)
|
/// let router = axum::Router::new()
|
||||||
/// .serve(
|
/// // Server side render the application, serve static assets, and register server functions
|
||||||
/// axum::Router::new()
|
/// .serve_dioxus_application(ServeConfig::default(), app)
|
||||||
/// // Server side render the application, serve static assets, and register server functions
|
/// .into_make_service();
|
||||||
/// .serve_dioxus_application(ServeConfig::new(), )
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
/// .into_make_service(),
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
/// )
|
|
||||||
/// .await
|
|
||||||
/// .unwrap();
|
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
/// fn app() -> Element {
|
/// fn app() -> Element {
|
||||||
/// unimplemented!()
|
/// rsx! { "Hello World" }
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
fn serve_dioxus_application(
|
fn serve_dioxus_application(self, cfg: impl Into<ServeConfig>, app: fn() -> Element) -> Self
|
||||||
self,
|
|
||||||
cfg: impl Into<ServeConfig>,
|
|
||||||
app: fn() -> Element,
|
|
||||||
) -> impl Future<Output = Self> + Send + Sync
|
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
@ -210,7 +186,7 @@ where
|
||||||
move |server_context| {
|
move |server_context| {
|
||||||
for context_provider in context_providers.iter() {
|
for context_provider in context_providers.iter() {
|
||||||
let context = context_provider();
|
let context = context_provider();
|
||||||
_ = server_context.insert_any(context);
|
server_context.insert_any(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
req,
|
req,
|
||||||
|
@ -229,77 +205,70 @@ where
|
||||||
|
|
||||||
// TODO: This is a breaking change, but we should probably serve static assets from a different directory than dist where the server executable is located
|
// TODO: This is a breaking change, but we should probably serve static assets from a different directory than dist where the server executable is located
|
||||||
// This would prevent issues like https://github.com/DioxusLabs/dioxus/issues/2327
|
// This would prevent issues like https://github.com/DioxusLabs/dioxus/issues/2327
|
||||||
fn serve_static_assets(
|
fn serve_static_assets(mut self, assets_path: impl Into<std::path::PathBuf>) -> Self {
|
||||||
mut self,
|
|
||||||
assets_path: impl Into<std::path::PathBuf>,
|
|
||||||
) -> impl Future<Output = Self> + Send + Sync {
|
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
|
|
||||||
let assets_path = assets_path.into();
|
let assets_path = assets_path.into();
|
||||||
async move {
|
|
||||||
// Serve all files in dist folder except index.html
|
|
||||||
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
|
|
||||||
panic!(
|
|
||||||
"Couldn't read assets directory at {:?}: {}",
|
|
||||||
&assets_path, e
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
for entry in dir.flatten() {
|
// Serve all files in dist folder except index.html
|
||||||
let path = entry.path();
|
let dir = std::fs::read_dir(&assets_path).unwrap_or_else(|e| {
|
||||||
if path.ends_with("index.html") {
|
panic!(
|
||||||
continue;
|
"Couldn't read assets directory at {:?}: {}",
|
||||||
}
|
&assets_path, e
|
||||||
let route = path
|
)
|
||||||
.strip_prefix(&assets_path)
|
});
|
||||||
.unwrap()
|
|
||||||
.iter()
|
for entry in dir.flatten() {
|
||||||
.map(|segment| {
|
let path = entry.path();
|
||||||
segment.to_str().unwrap_or_else(|| {
|
if path.ends_with("index.html") {
|
||||||
panic!("Failed to convert path segment {:?} to string", segment)
|
continue;
|
||||||
})
|
}
|
||||||
})
|
let route = path
|
||||||
.collect::<Vec<_>>()
|
.strip_prefix(&assets_path)
|
||||||
.join("/");
|
.unwrap()
|
||||||
let route = format!("/{}", route);
|
.iter()
|
||||||
if path.is_dir() {
|
.map(|segment| {
|
||||||
self = self.nest_service(&route, ServeDir::new(path).precompressed_br());
|
segment.to_str().unwrap_or_else(|| {
|
||||||
} else {
|
panic!("Failed to convert path segment {:?} to string", segment)
|
||||||
self = self.nest_service(&route, ServeFile::new(path).precompressed_br());
|
})
|
||||||
}
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("/");
|
||||||
|
let route = format!("/{}", route);
|
||||||
|
if path.is_dir() {
|
||||||
|
self = self.nest_service(&route, ServeDir::new(path).precompressed_br());
|
||||||
|
} else {
|
||||||
|
self = self.nest_service(&route, ServeFile::new(path).precompressed_br());
|
||||||
}
|
}
|
||||||
|
|
||||||
self
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn serve_dioxus_application(
|
fn serve_dioxus_application(self, cfg: impl Into<ServeConfig>, app: fn() -> Element) -> Self {
|
||||||
self,
|
|
||||||
cfg: impl Into<ServeConfig>,
|
|
||||||
app: fn() -> Element,
|
|
||||||
) -> impl Future<Output = Self> + Send + Sync {
|
|
||||||
let cfg = cfg.into();
|
let cfg = cfg.into();
|
||||||
async move {
|
|
||||||
let ssr_state = SSRState::new(&cfg);
|
|
||||||
|
|
||||||
// Add server functions and render index.html
|
let ssr_state = SSRState::new(&cfg);
|
||||||
let mut server = self
|
|
||||||
.serve_static_assets(cfg.assets_path.clone())
|
|
||||||
.await
|
|
||||||
.register_server_functions();
|
|
||||||
|
|
||||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
// Add server functions and render index.html
|
||||||
{
|
#[allow(unused_mut)]
|
||||||
use dioxus_hot_reload::HotReloadRouterExt;
|
let mut server = self
|
||||||
server = server.forward_cli_hot_reloading();
|
.serve_static_assets(cfg.assets_path.clone())
|
||||||
}
|
.register_server_functions();
|
||||||
|
|
||||||
server.fallback(get(render_handler).with_state((
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
cfg,
|
{
|
||||||
Arc::new(move || VirtualDom::new(app)),
|
use dioxus_hot_reload::HotReloadRouterExt;
|
||||||
ssr_state,
|
server = server.forward_cli_hot_reloading();
|
||||||
)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server.fallback(
|
||||||
|
get(render_handler).with_state(
|
||||||
|
RenderHandleState::new(app)
|
||||||
|
.with_config(cfg)
|
||||||
|
.with_ssr_state(ssr_state),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,12 +282,54 @@ fn apply_request_parts_to_response<B>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AxumHandler<F> = (
|
/// State used by [`render_handler`] to render a dioxus component with axum
|
||||||
F,
|
#[derive(Clone)]
|
||||||
ServeConfig,
|
pub struct RenderHandleState {
|
||||||
SSRState,
|
config: ServeConfig,
|
||||||
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
build_virtual_dom: Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
||||||
);
|
ssr_state: once_cell::sync::OnceCell<SSRState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderHandleState {
|
||||||
|
/// Create a new [`RenderHandleState`]
|
||||||
|
pub fn new(root: fn() -> Element) -> Self {
|
||||||
|
Self {
|
||||||
|
config: ServeConfig::default(),
|
||||||
|
build_virtual_dom: Arc::new(move || VirtualDom::new(root)),
|
||||||
|
ssr_state: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new [`RenderHandleState`] with a custom [`VirtualDom`] factory. This method can be used to pass context into the root component of your application.
|
||||||
|
pub fn new_with_virtual_dom_factory(
|
||||||
|
build_virtual_dom: impl Fn() -> VirtualDom + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
config: ServeConfig::default(),
|
||||||
|
build_virtual_dom: Arc::new(build_virtual_dom),
|
||||||
|
ssr_state: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the [`ServeConfig`] for this [`RenderHandleState`]
|
||||||
|
pub fn with_config(mut self, config: ServeConfig) -> Self {
|
||||||
|
self.config = config;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the [`SSRState`] for this [`RenderHandleState`]. Sharing a [`SSRState`] between multiple [`RenderHandleState`]s is more efficient than creating a new [`SSRState`] for each [`RenderHandleState`].
|
||||||
|
pub fn with_ssr_state(mut self, ssr_state: SSRState) -> Self {
|
||||||
|
self.ssr_state = once_cell::sync::OnceCell::new();
|
||||||
|
if self.ssr_state.set(ssr_state).is_err() {
|
||||||
|
panic!("SSRState already set");
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ssr_state(&self) -> &SSRState {
|
||||||
|
self.ssr_state.get_or_init(|| SSRState::new(&self.config))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// SSR renderer handler for Axum with added context injection.
|
/// SSR renderer handler for Axum with added context injection.
|
||||||
///
|
///
|
||||||
|
@ -328,8 +339,7 @@ type AxumHandler<F> = (
|
||||||
/// use std::sync::{Arc, Mutex};
|
/// use std::sync::{Arc, Mutex};
|
||||||
///
|
///
|
||||||
/// use axum::routing::get;
|
/// use axum::routing::get;
|
||||||
/// use dioxus_lib::prelude::*;
|
/// use dioxus::prelude::*;
|
||||||
/// use dioxus_fullstack::{axum_adapter::render_handler_with_context, prelude::*};
|
|
||||||
///
|
///
|
||||||
/// fn app() -> Element {
|
/// fn app() -> Element {
|
||||||
/// rsx! {
|
/// rsx! {
|
||||||
|
@ -339,82 +349,61 @@ type AxumHandler<F> = (
|
||||||
///
|
///
|
||||||
/// #[tokio::main]
|
/// #[tokio::main]
|
||||||
/// async fn main() {
|
/// async fn main() {
|
||||||
/// let cfg = ServerConfig::new(app, ())
|
|
||||||
/// .assets_path("dist")
|
|
||||||
/// .build();
|
|
||||||
/// let ssr_state = SSRState::new(&cfg);
|
|
||||||
///
|
|
||||||
/// // This could be any state you want to be accessible from your server
|
|
||||||
/// // functions using `[DioxusServerContext::get]`.
|
|
||||||
/// let state = Arc::new(Mutex::new("state".to_string()));
|
|
||||||
///
|
|
||||||
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
/// let addr = std::net::SocketAddr::from(([127, 0, 0, 1], 8080));
|
||||||
/// axum::Server::bind(&addr)
|
/// let router = axum::Router::new()
|
||||||
/// .serve(
|
/// // Register server functions, etc.
|
||||||
/// axum::Router::new()
|
/// // Note you can use `register_server_functions_with_context`
|
||||||
/// // Register server functions, etc.
|
/// // to inject the context into server functions running outside
|
||||||
/// // Note you can use `register_server_functions_with_context`
|
/// // of an SSR render context.
|
||||||
/// // to inject the context into server functions running outside
|
/// .fallback(get(render_handler)
|
||||||
/// // of an SSR render context.
|
/// .with_state(RenderHandleState::new(app))
|
||||||
/// .fallback(get(render_handler_with_context).with_state((
|
|
||||||
/// move |ctx| ctx.insert(state.clone()).unwrap(),
|
|
||||||
/// cfg,
|
|
||||||
/// ssr_state,
|
|
||||||
/// )))
|
|
||||||
/// .into_make_service(),
|
|
||||||
/// )
|
/// )
|
||||||
/// .await
|
/// .into_make_service();
|
||||||
/// .unwrap();
|
/// let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
/// axum::serve(listener, router).await.unwrap();
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub async fn render_handler_with_context<F: FnMut(&mut DioxusServerContext)>(
|
pub async fn render_handler(
|
||||||
State((mut inject_context, cfg, ssr_state, virtual_dom_factory)): State<AxumHandler<F>>,
|
State(state): State<RenderHandleState>,
|
||||||
request: Request<Body>,
|
request: Request<Body>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
|
// Only respond to requests for HTML
|
||||||
|
if let Some(mime) = request.headers().get("Accept") {
|
||||||
|
let mime = mime.to_str().map(|mime| mime.to_ascii_lowercase());
|
||||||
|
match mime {
|
||||||
|
Ok(accepts) if accepts.contains("text/html") => {}
|
||||||
|
_ => return Err(StatusCode::NOT_ACCEPTABLE),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cfg = &state.config;
|
||||||
|
let ssr_state = state.ssr_state();
|
||||||
|
let build_virtual_dom = state.build_virtual_dom.clone();
|
||||||
|
|
||||||
let (parts, _) = request.into_parts();
|
let (parts, _) = request.into_parts();
|
||||||
let url = parts.uri.path_and_query().unwrap().to_string();
|
let url = parts.uri.path_and_query().unwrap().to_string();
|
||||||
let parts: Arc<tokio::sync::RwLock<http::request::Parts>> =
|
let parts: Arc<parking_lot::RwLock<http::request::Parts>> =
|
||||||
Arc::new(tokio::sync::RwLock::new(parts));
|
Arc::new(parking_lot::RwLock::new(parts));
|
||||||
let mut server_context = DioxusServerContext::new(parts.clone());
|
let server_context = DioxusServerContext::from_shared_parts(parts.clone());
|
||||||
inject_context(&mut server_context);
|
|
||||||
|
|
||||||
match ssr_state
|
match ssr_state
|
||||||
.render(url, &cfg, move || virtual_dom_factory(), &server_context)
|
.render(url, cfg, move || build_virtual_dom(), &server_context)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(rendered) => {
|
Ok((freshness, rx)) => {
|
||||||
let crate::render::RenderResponse { html, freshness } = rendered;
|
let mut response = axum::response::Html::from(Body::from_stream(rx)).into_response();
|
||||||
let mut response = axum::response::Html::from(html).into_response();
|
|
||||||
freshness.write(response.headers_mut());
|
freshness.write(response.headers_mut());
|
||||||
let headers = server_context.response_parts().unwrap().headers.clone();
|
let headers = server_context.response_parts().headers.clone();
|
||||||
apply_request_parts_to_response(headers, &mut response);
|
apply_request_parts_to_response(headers, &mut response);
|
||||||
response
|
Ok(response)
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!("Failed to render page: {}", e);
|
tracing::error!("Failed to render page: {}", e);
|
||||||
report_err(e).into_response()
|
Ok(report_err(e).into_response())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type RenderHandlerExtractor = (
|
|
||||||
ServeConfig,
|
|
||||||
Arc<dyn Fn() -> VirtualDom + Send + Sync>,
|
|
||||||
SSRState,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// SSR renderer handler for Axum
|
|
||||||
pub async fn render_handler(
|
|
||||||
State((cfg, virtual_dom_factory, ssr_state)): State<RenderHandlerExtractor>,
|
|
||||||
request: Request<Body>,
|
|
||||||
) -> impl IntoResponse {
|
|
||||||
render_handler_with_context(
|
|
||||||
State((|_: &mut _| (), cfg, ssr_state, virtual_dom_factory)),
|
|
||||||
request,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
|
fn report_err<E: std::fmt::Display>(e: E) -> Response<axum::body::Body> {
|
||||||
Response::builder()
|
Response::builder()
|
||||||
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
.status(StatusCode::INTERNAL_SERVER_ERROR)
|
||||||
|
@ -439,7 +428,7 @@ async fn handle_server_fns_inner(
|
||||||
if let Some(mut service) =
|
if let Some(mut service) =
|
||||||
server_fn::axum::get_server_fn_service(&path_string)
|
server_fn::axum::get_server_fn_service(&path_string)
|
||||||
{
|
{
|
||||||
let server_context = DioxusServerContext::new(Arc::new(tokio::sync::RwLock::new(parts)));
|
let server_context = DioxusServerContext::new(parts);
|
||||||
additional_context(&server_context);
|
additional_context(&server_context);
|
||||||
|
|
||||||
// store Accepts and Referrer in case we need them for redirect (below)
|
// store Accepts and Referrer in case we need them for redirect (below)
|
||||||
|
@ -467,7 +456,7 @@ async fn handle_server_fns_inner(
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply the response parts from the server context to the response
|
// apply the response parts from the server context to the response
|
||||||
let mut res_options = server_context.response_parts_mut().unwrap();
|
let mut res_options = server_context.response_parts_mut();
|
||||||
res.headers_mut().extend(res_options.headers.drain());
|
res.headers_mut().extend(res_options.headers.drain());
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
use dioxus_lib::prelude::use_hook;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
/// This allows you to send data from the server to the client. The data is serialized into the HTML on the server and hydrated on the client.
|
/// This allows you to send data from the server to the client. The data is serialized into the HTML on the server and hydrated on the client.
|
||||||
|
@ -12,25 +13,36 @@ use serde::{de::DeserializeOwned, Serialize};
|
||||||
/// use dioxus_fullstack::prelude::*;
|
/// use dioxus_fullstack::prelude::*;
|
||||||
///
|
///
|
||||||
/// fn app() -> Element {
|
/// fn app() -> Element {
|
||||||
/// let state1 = server_cached(|| {
|
/// let state1 = use_server_cached(|| {
|
||||||
/// 1234
|
/// 1234
|
||||||
/// });
|
/// });
|
||||||
///
|
///
|
||||||
/// None
|
/// todo!()
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub fn server_cached<O: 'static + Serialize + DeserializeOwned>(server_fn: impl Fn() -> O) -> O {
|
pub fn use_server_cached<O: 'static + Clone + Serialize + DeserializeOwned>(
|
||||||
|
server_fn: impl Fn() -> O,
|
||||||
|
) -> O {
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
{
|
{
|
||||||
let data = server_fn();
|
let serialize = crate::html_storage::use_serialize_context();
|
||||||
let sc = crate::prelude::server_context();
|
use_hook(|| {
|
||||||
if let Err(err) = sc.push_html_data(&data) {
|
let data = server_fn();
|
||||||
tracing::error!("Failed to push HTML data: {}", err);
|
serialize.push(&data);
|
||||||
}
|
data
|
||||||
data
|
})
|
||||||
}
|
}
|
||||||
#[cfg(not(feature = "server"))]
|
#[cfg(all(not(feature = "server"), feature = "web"))]
|
||||||
{
|
{
|
||||||
crate::html_storage::deserialize::take_server_data().unwrap_or_else(server_fn)
|
use_hook(|| {
|
||||||
|
dioxus_web::take_server_data()
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
.unwrap_or_else(server_fn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[cfg(not(any(feature = "server", feature = "web")))]
|
||||||
|
{
|
||||||
|
use_hook(server_fn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,46 +2,121 @@ use dioxus_lib::prelude::*;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
use std::future::Future;
|
use std::future::Future;
|
||||||
|
|
||||||
/// A future that resolves to a value.
|
/// Runs a future with a manual list of dependencies and returns a resource with the result if the future is finished or a suspended error if it is still running.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// On the server, this will wait until the future is resolved before continuing to render. When the future is resolved, the result will be serialized into the page and hydrated on the client without rerunning the future.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// <div class="warning">
|
||||||
|
///
|
||||||
|
/// Unlike [`use_resource`] dependencies are only tracked inside the function that spawns the async block, not the async block itself.
|
||||||
|
///
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// // ❌ The future inside of use_server_future is not reactive
|
||||||
|
/// let id = use_signal(|| 0);
|
||||||
|
/// use_server_future(move || {
|
||||||
|
/// async move {
|
||||||
|
/// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||||
|
/// println!("{id}");
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
/// // ✅ The closure that creates the future for use_server_future is reactive
|
||||||
|
/// let id = use_signal(|| 0);
|
||||||
|
/// use_server_future(move || {
|
||||||
|
/// // The closure itself is reactive which means the future will subscribe to any signals you read here
|
||||||
|
/// let cloned_id = id();
|
||||||
|
/// async move {
|
||||||
|
/// // But the future is not reactive which means that the future will not subscribe to any reads here
|
||||||
|
/// println!("{cloned_id}");
|
||||||
|
/// }
|
||||||
|
/// });
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// </div>
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// ```rust, no_run
|
||||||
|
/// # use dioxus::prelude::*;
|
||||||
|
/// # async fn fetch_article(id: u32) -> String { todo!() }
|
||||||
|
/// use dioxus::prelude::*;
|
||||||
|
///
|
||||||
|
/// fn App() -> Element {
|
||||||
|
/// let mut article_id = use_signal(|| 0);
|
||||||
|
/// // `use_server_future` will spawn a task that runs on the server and serializes the result to send to the client.
|
||||||
|
/// // The future will rerun any time the
|
||||||
|
/// // Since we bubble up the suspense with `?`, the server will wait for the future to resolve before rendering
|
||||||
|
/// let article = use_server_future(move || fetch_article(article_id()))?;
|
||||||
|
///
|
||||||
|
/// rsx! {
|
||||||
|
/// "{article().unwrap()}"
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
#[must_use = "Consider using `cx.spawn` to run a future without reading its value"]
|
||||||
pub fn use_server_future<T, F>(_future: impl FnMut() -> F + 'static) -> Option<Resource<T>>
|
pub fn use_server_future<T, F>(
|
||||||
|
mut future: impl FnMut() -> F + 'static,
|
||||||
|
) -> Result<Resource<T>, RenderError>
|
||||||
where
|
where
|
||||||
T: Serialize + DeserializeOwned + 'static,
|
T: Serialize + DeserializeOwned + 'static,
|
||||||
F: Future<Output = T> + 'static,
|
F: Future<Output = T> + 'static,
|
||||||
{
|
{
|
||||||
let cb = use_callback(_future);
|
#[cfg(feature = "server")]
|
||||||
let mut first_run = use_hook(|| CopyValue::new(true));
|
let serialize_context = crate::html_storage::use_serialize_context();
|
||||||
|
// We always create a storage entry, even if the data isn't ready yet to make it possible to deserialize pending server futures on the client
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
let server_storage_entry = use_hook(|| serialize_context.create_entry());
|
||||||
|
|
||||||
|
// If this is the first run and we are on the web client, the data might be cached
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
let initial_web_result = use_hook(|| {
|
||||||
|
tracing::info!("First run of use_server_future");
|
||||||
|
|
||||||
|
std::rc::Rc::new(std::cell::RefCell::new(Some(
|
||||||
|
dioxus_web::take_server_data::<T>(),
|
||||||
|
)))
|
||||||
|
});
|
||||||
|
|
||||||
let resource = use_resource(move || {
|
let resource = use_resource(move || {
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
let serialize_context = serialize_context.clone();
|
||||||
|
let user_fut = future();
|
||||||
|
#[cfg(feature = "web")]
|
||||||
|
let initial_web_result = initial_web_result.clone();
|
||||||
|
|
||||||
async move {
|
async move {
|
||||||
let user_fut = cb.call();
|
|
||||||
|
|
||||||
let currently_in_first_run = first_run.cloned();
|
|
||||||
|
|
||||||
// If this is the first run and we are on the web client, the data might be cached
|
// If this is the first run and we are on the web client, the data might be cached
|
||||||
if currently_in_first_run {
|
#[cfg(feature = "web")]
|
||||||
tracing::info!("First run of use_server_future");
|
{
|
||||||
// This is no longer the first run
|
let initial = initial_web_result.borrow_mut().take();
|
||||||
first_run.set(false);
|
match initial {
|
||||||
|
// This isn't the first run
|
||||||
#[cfg(feature = "web")]
|
None => {}
|
||||||
if let Some(o) = crate::html_storage::deserialize::take_server_data::<T>() {
|
// This is the first run
|
||||||
// this is going to subscribe this resource to any reactivity given to use in the callback
|
Some(first_run) => {
|
||||||
// We're doing this regardless so inputs get tracked, even if we drop the future before polling it
|
match first_run {
|
||||||
kick_future(user_fut);
|
// THe data was deserialized successfully from the server
|
||||||
|
Ok(Some(o)) => return o,
|
||||||
return o;
|
// The data is still pending from the server. Don't try to resolve it on the client
|
||||||
|
Ok(None) => {
|
||||||
|
tracing::trace!("Waiting for server data");
|
||||||
|
std::future::pending::<()>().await;
|
||||||
|
}
|
||||||
|
// The data was not available on the server, rerun the future
|
||||||
|
Err(_) => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise just run the future itself
|
// Otherwise just run the future itself
|
||||||
let out = user_fut.await;
|
let out = user_fut.await;
|
||||||
|
|
||||||
// If this is the first run and we are on the server, cache the data
|
// If this is the first run and we are on the server, cache the data in the slot we reserved for it
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
if currently_in_first_run {
|
serialize_context.insert(server_storage_entry, &out);
|
||||||
let _ = crate::server_context::server_context().push_html_data(&out);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::let_and_return)]
|
#[allow(clippy::let_and_return)]
|
||||||
out
|
out
|
||||||
|
@ -56,24 +131,12 @@ where
|
||||||
// Suspend if the value isn't ready
|
// Suspend if the value isn't ready
|
||||||
match resource.state().cloned() {
|
match resource.state().cloned() {
|
||||||
UseResourceState::Pending => {
|
UseResourceState::Pending => {
|
||||||
suspend(resource.task());
|
let task = resource.task();
|
||||||
None
|
if !task.paused() {
|
||||||
|
return Err(suspend(task).unwrap_err());
|
||||||
|
}
|
||||||
|
Ok(resource)
|
||||||
}
|
}
|
||||||
_ => Some(resource),
|
_ => Ok(resource),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "web")]
|
|
||||||
#[inline]
|
|
||||||
fn kick_future<F, T>(user_fut: F)
|
|
||||||
where
|
|
||||||
F: Future<Output = T> + 'static,
|
|
||||||
{
|
|
||||||
// Kick the future to subscribe its dependencies
|
|
||||||
use futures_util::future::FutureExt;
|
|
||||||
let waker = futures_util::task::noop_waker();
|
|
||||||
let mut cx = std::task::Context::from_waker(&waker);
|
|
||||||
futures_util::pin_mut!(user_fut);
|
|
||||||
|
|
||||||
let _ = user_fut.poll_unpin(&mut cx);
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
|
|
||||||
use base64::engine::general_purpose::STANDARD;
|
|
||||||
use base64::Engine;
|
|
||||||
|
|
||||||
use super::HTMLDataCursor;
|
|
||||||
|
|
||||||
#[allow(unused)]
|
|
||||||
pub(crate) fn serde_from_bytes<T: DeserializeOwned>(string: &[u8]) -> Option<T> {
|
|
||||||
let decompressed = match STANDARD.decode(string) {
|
|
||||||
Ok(bytes) => bytes,
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Failed to decode base64: {}", err);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match ciborium::from_reader(std::io::Cursor::new(decompressed)) {
|
|
||||||
Ok(data) => Some(data),
|
|
||||||
Err(err) => {
|
|
||||||
tracing::error!("Failed to deserialize: {}", err);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static SERVER_DATA: once_cell::sync::Lazy<Option<HTMLDataCursor>> =
|
|
||||||
once_cell::sync::Lazy::new(|| {
|
|
||||||
#[cfg(all(feature = "web", target_arch = "wasm32"))]
|
|
||||||
{
|
|
||||||
let window = web_sys::window()?.document()?;
|
|
||||||
let element = match window.get_element_by_id("dioxus-storage-data") {
|
|
||||||
Some(element) => element,
|
|
||||||
None => {
|
|
||||||
tracing::error!("Failed to get element by id: dioxus-storage-data");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let attribute = match element.get_attribute("data-serialized") {
|
|
||||||
Some(attribute) => attribute,
|
|
||||||
None => {
|
|
||||||
tracing::error!("Failed to get attribute: data-serialized");
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let data: super::HTMLData = serde_from_bytes(attribute.as_bytes())?;
|
|
||||||
|
|
||||||
Some(data.cursor())
|
|
||||||
}
|
|
||||||
#[cfg(not(all(feature = "web", target_arch = "wasm32")))]
|
|
||||||
{
|
|
||||||
None
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
pub(crate) fn take_server_data<T: DeserializeOwned>() -> Option<T> {
|
|
||||||
SERVER_DATA.as_ref()?.take()
|
|
||||||
}
|
|
|
@ -1,112 +1,65 @@
|
||||||
#![allow(unused)]
|
#![allow(unused)]
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use std::{io::Cursor, sync::atomic::AtomicUsize};
|
use dioxus_lib::prelude::{has_context, provide_context, use_hook};
|
||||||
|
use serialize::serde_to_writable;
|
||||||
|
use std::{cell::RefCell, io::Cursor, rc::Rc, sync::atomic::AtomicUsize};
|
||||||
|
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
use serde::{de::DeserializeOwned, Serialize};
|
use serde::{de::DeserializeOwned, Serialize};
|
||||||
|
|
||||||
pub(crate) mod deserialize;
|
|
||||||
|
|
||||||
pub(crate) mod serialize;
|
pub(crate) mod serialize;
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
pub(crate) struct SerializeContext {
|
||||||
|
data: Rc<RefCell<HTMLData>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SerializeContext {
|
||||||
|
/// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
|
||||||
|
pub(crate) fn create_entry(&self) -> usize {
|
||||||
|
self.data.borrow_mut().create_entry()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert data into an entry that was created with [`Self::create_entry`]
|
||||||
|
pub(crate) fn insert<T: Serialize>(&self, id: usize, value: &T) {
|
||||||
|
self.data.borrow_mut().insert(id, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Push resolved data into the serialized server data
|
||||||
|
pub(crate) fn push<T: Serialize>(&self, data: &T) {
|
||||||
|
self.data.borrow_mut().push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn use_serialize_context() -> SerializeContext {
|
||||||
|
use_hook(|| has_context().unwrap_or_else(|| provide_context(SerializeContext::default())))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
#[derive(serde::Serialize, serde::Deserialize, Default)]
|
||||||
|
#[serde(transparent)]
|
||||||
pub(crate) struct HTMLData {
|
pub(crate) struct HTMLData {
|
||||||
pub data: Vec<Vec<u8>>,
|
pub data: Vec<Option<Vec<u8>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HTMLData {
|
impl HTMLData {
|
||||||
pub(crate) fn push<T: Serialize>(&mut self, value: &T) {
|
/// Create a new entry in the data that will be sent to the client without inserting any data. Returns an id that can be used to insert data into the entry once it is ready.
|
||||||
|
pub(crate) fn create_entry(&mut self) -> usize {
|
||||||
|
let id = self.data.len();
|
||||||
|
self.data.push(None);
|
||||||
|
id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Insert data into an entry that was created with [`Self::create_entry`]
|
||||||
|
pub(crate) fn insert<T: Serialize>(&mut self, id: usize, value: &T) {
|
||||||
let mut serialized = Vec::new();
|
let mut serialized = Vec::new();
|
||||||
serialize::serde_to_writable(value, &mut serialized).unwrap();
|
ciborium::into_writer(value, &mut serialized).unwrap();
|
||||||
self.data.push(serialized);
|
self.data[id] = Some(serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn cursor(self) -> HTMLDataCursor {
|
/// Push resolved data into the serialized server data
|
||||||
HTMLDataCursor {
|
pub(crate) fn push<T: Serialize>(&mut self, data: &T) {
|
||||||
data: self.data,
|
let mut serialized = Vec::new();
|
||||||
index: AtomicUsize::new(0),
|
ciborium::into_writer(data, &mut serialized).unwrap();
|
||||||
}
|
self.data.push(Some(serialized));
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) struct HTMLDataCursor {
|
|
||||||
data: Vec<Vec<u8>>,
|
|
||||||
index: AtomicUsize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HTMLDataCursor {
|
|
||||||
pub fn take<T: DeserializeOwned>(&self) -> Option<T> {
|
|
||||||
let current = self.index.load(std::sync::atomic::Ordering::SeqCst);
|
|
||||||
if current >= self.data.len() {
|
|
||||||
tracing::error!(
|
|
||||||
"Tried to take more data than was available, len: {}, index: {}",
|
|
||||||
self.data.len(),
|
|
||||||
current
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut cursor = &self.data[current];
|
|
||||||
let mut decoded = STANDARD.decode(cursor).unwrap();
|
|
||||||
self.index.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
|
|
||||||
match ciborium::from_reader(Cursor::new(decoded)) {
|
|
||||||
Ok(x) => Some(x),
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("Error deserializing data: {:?}", e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn serialized_and_deserializes() {
|
|
||||||
use ciborium::{from_reader, into_writer};
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
|
||||||
struct Data {
|
|
||||||
a: u32,
|
|
||||||
b: String,
|
|
||||||
bytes: Vec<u8>,
|
|
||||||
nested: Nested,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
|
||||||
struct Nested {
|
|
||||||
a: u32,
|
|
||||||
b: u16,
|
|
||||||
c: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
for x in 0..10usize {
|
|
||||||
for y in 0..10 {
|
|
||||||
let mut as_string: Vec<u8> = Vec::new();
|
|
||||||
let data = vec![
|
|
||||||
Data {
|
|
||||||
a: x as u32,
|
|
||||||
b: "hello".to_string(),
|
|
||||||
bytes: vec![0; x],
|
|
||||||
nested: Nested {
|
|
||||||
a: 1,
|
|
||||||
b: x as u16,
|
|
||||||
c: 3
|
|
||||||
},
|
|
||||||
};
|
|
||||||
y
|
|
||||||
];
|
|
||||||
serialize::serde_to_writable(&data, &mut as_string).unwrap();
|
|
||||||
|
|
||||||
println!("{:?}", as_string);
|
|
||||||
println!(
|
|
||||||
"original size: {}",
|
|
||||||
std::mem::size_of::<Data>() * data.len()
|
|
||||||
);
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
into_writer(&data, &mut bytes).unwrap();
|
|
||||||
println!("serialized size: {}", bytes.len());
|
|
||||||
println!("compressed size: {}", as_string.len());
|
|
||||||
|
|
||||||
let decoded: Vec<Data> = deserialize::serde_from_bytes(&as_string).unwrap();
|
|
||||||
assert_eq!(data, decoded);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,88 @@
|
||||||
|
use dioxus_lib::prelude::dioxus_core::DynamicNode;
|
||||||
|
use dioxus_lib::prelude::{
|
||||||
|
has_context, try_consume_context, ScopeId, SuspenseBoundaryProps, VNode, VirtualDom,
|
||||||
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use base64::engine::general_purpose::STANDARD;
|
use base64::engine::general_purpose::STANDARD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
|
||||||
|
use super::SerializeContext;
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub(crate) fn serde_to_writable<T: Serialize>(
|
pub(crate) fn serde_to_writable<T: Serialize>(
|
||||||
value: &T,
|
value: &T,
|
||||||
write_to: &mut impl std::io::Write,
|
write_to: &mut impl std::fmt::Write,
|
||||||
) -> Result<(), ciborium::ser::Error<std::io::Error>> {
|
) -> Result<(), ciborium::ser::Error<std::fmt::Error>> {
|
||||||
let mut serialized = Vec::new();
|
let mut serialized = Vec::new();
|
||||||
ciborium::into_writer(value, &mut serialized)?;
|
ciborium::into_writer(value, &mut serialized).unwrap();
|
||||||
write_to.write_all(STANDARD.encode(serialized).as_bytes())?;
|
write_to.write_str(STANDARD.encode(serialized).as_str())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
impl super::HTMLData {
|
||||||
/// Encode data into a element. This is inteded to be used in the server to send data to the client.
|
/// Walks through the suspense boundary in a depth first order and extracts the data from the context API.
|
||||||
pub(crate) fn encode_in_element(
|
/// We use depth first order instead of relying on the order the hooks are called in because during suspense on the server, the order that futures are run in may be non deterministic.
|
||||||
data: &super::HTMLData,
|
pub(crate) fn extract_from_suspense_boundary(vdom: &VirtualDom, scope: ScopeId) -> Self {
|
||||||
write_to: &mut impl std::io::Write,
|
let mut data = Self::default();
|
||||||
) -> Result<(), ciborium::ser::Error<std::io::Error>> {
|
data.take_from_scope(vdom, scope);
|
||||||
write_to.write_all(
|
data
|
||||||
r#"<meta hidden="true" id="dioxus-storage-data" data-serialized=""#.as_bytes(),
|
}
|
||||||
)?;
|
|
||||||
serde_to_writable(&data, write_to)?;
|
fn take_from_virtual_dom(&mut self, vdom: &VirtualDom) {
|
||||||
Ok(write_to.write_all(r#"" />"#.as_bytes())?)
|
self.take_from_scope(vdom, ScopeId::ROOT)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_from_scope(&mut self, vdom: &VirtualDom, scope: ScopeId) {
|
||||||
|
vdom.in_runtime(|| {
|
||||||
|
scope.in_runtime(|| {
|
||||||
|
// Grab any serializable server context from this scope
|
||||||
|
let context: Option<SerializeContext> = has_context();
|
||||||
|
if let Some(context) = context {
|
||||||
|
let borrow = context.data.borrow();
|
||||||
|
let mut data = borrow.data.iter().cloned();
|
||||||
|
self.data.extend(data)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// then continue to any children
|
||||||
|
if let Some(scope) = vdom.get_scope(scope) {
|
||||||
|
// If this is a suspense boundary, move into the children first (even if they are suspended) because that will be run first on the client
|
||||||
|
if let Some(suspense_boundary) = SuspenseBoundaryProps::downcast_from_scope(scope) {
|
||||||
|
if let Some(node) = suspense_boundary.suspended_nodes.as_ref() {
|
||||||
|
self.take_from_vnode(vdom, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(node) = scope.try_root_node() {
|
||||||
|
self.take_from_vnode(vdom, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn take_from_vnode(&mut self, vdom: &VirtualDom, vnode: &VNode) {
|
||||||
|
for (dynamic_node_index, dyn_node) in vnode.dynamic_nodes.iter().enumerate() {
|
||||||
|
match dyn_node {
|
||||||
|
DynamicNode::Component(comp) => {
|
||||||
|
if let Some(scope) = comp.mounted_scope(dynamic_node_index, vnode, vdom) {
|
||||||
|
self.take_from_scope(vdom, scope.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicNode::Fragment(nodes) => {
|
||||||
|
for node in nodes {
|
||||||
|
self.take_from_vnode(vdom, node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
/// Encode data as base64. This is intended to be used in the server to send data to the client.
|
||||||
|
pub(crate) fn serialized(&self) -> String {
|
||||||
|
let mut serialized = Vec::new();
|
||||||
|
ciborium::into_writer(&self.data, &mut serialized).unwrap();
|
||||||
|
base64::engine::general_purpose::STANDARD.encode(serialized)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,10 +5,13 @@ use std::{any::Any, sync::Arc};
|
||||||
use dioxus_lib::prelude::{Element, VirtualDom};
|
use dioxus_lib::prelude::{Element, VirtualDom};
|
||||||
|
|
||||||
pub use crate::Config;
|
pub use crate::Config;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub(crate) type ContextProviders = Arc<
|
pub(crate) type ContextProviders = Arc<
|
||||||
Vec<Box<dyn Fn() -> Box<dyn std::any::Any + Send + Sync + 'static> + Send + Sync + 'static>>,
|
Vec<Box<dyn Fn() -> Box<dyn std::any::Any + Send + Sync + 'static> + Send + Sync + 'static>>,
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
fn virtual_dom_factory(
|
fn virtual_dom_factory(
|
||||||
root: fn() -> Element,
|
root: fn() -> Element,
|
||||||
contexts: ContextProviders,
|
contexts: ContextProviders,
|
||||||
|
@ -87,6 +90,22 @@ pub fn launch(
|
||||||
dioxus_mobile::launch::launch_virtual_dom(factory(), cfg)
|
dioxus_mobile::launch::launch_virtual_dom(factory(), cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(
|
||||||
|
feature = "server",
|
||||||
|
feature = "web",
|
||||||
|
feature = "desktop",
|
||||||
|
feature = "mobile"
|
||||||
|
)))]
|
||||||
|
/// Launch a fullstack app with the given root component, contexts, and config.
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn launch(
|
||||||
|
root: fn() -> Element,
|
||||||
|
contexts: Vec<Box<dyn Fn() -> Box<dyn Any + Send + Sync> + Send + Sync>>,
|
||||||
|
platform_config: Config,
|
||||||
|
) -> ! {
|
||||||
|
panic!("No platform feature enabled. Please enable one of the following features: axum, desktop, or web to use the launch API.")
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
/// Launch a server application
|
/// Launch a server application
|
||||||
|
@ -97,6 +116,8 @@ async fn launch_server(
|
||||||
) {
|
) {
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
|
use crate::prelude::RenderHandleState;
|
||||||
|
|
||||||
let args = dioxus_cli_config::ServeArguments::from_cli()
|
let args = dioxus_cli_config::ServeArguments::from_cli()
|
||||||
.unwrap_or_else(dioxus_cli_config::ServeArguments::parse);
|
.unwrap_or_else(dioxus_cli_config::ServeArguments::parse);
|
||||||
let addr = args
|
let addr = args
|
||||||
|
@ -116,8 +137,7 @@ async fn launch_server(
|
||||||
|
|
||||||
let cfg = platform_config.server_cfg.build();
|
let cfg = platform_config.server_cfg.build();
|
||||||
|
|
||||||
let ssr_state = SSRState::new(&cfg);
|
let mut router = router.serve_static_assets(cfg.assets_path.clone());
|
||||||
let mut router = router.serve_static_assets(cfg.assets_path.clone()).await;
|
|
||||||
|
|
||||||
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
#[cfg(all(feature = "hot-reload", debug_assertions))]
|
||||||
{
|
{
|
||||||
|
@ -126,11 +146,10 @@ async fn launch_server(
|
||||||
}
|
}
|
||||||
|
|
||||||
router.fallback(
|
router.fallback(
|
||||||
axum::routing::get(crate::axum_adapter::render_handler).with_state((
|
axum::routing::get(crate::axum_adapter::render_handler).with_state(
|
||||||
cfg,
|
RenderHandleState::new_with_virtual_dom_factory(build_virtual_dom)
|
||||||
Arc::new(build_virtual_dom),
|
.with_config(cfg),
|
||||||
ssr_state,
|
),
|
||||||
)),
|
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
let router = router.into_make_service();
|
let router = router.into_make_service();
|
||||||
|
|
|
@ -20,6 +20,8 @@ pub use config::*;
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
mod render;
|
mod render;
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
mod streaming;
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
mod serve_config;
|
mod serve_config;
|
||||||
|
@ -32,7 +34,7 @@ mod server_context;
|
||||||
/// A prelude of commonly used items in dioxus-fullstack.
|
/// A prelude of commonly used items in dioxus-fullstack.
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
use crate::hooks;
|
use crate::hooks;
|
||||||
pub use hooks::{server_cached::server_cached, server_future::use_server_future};
|
pub use hooks::{server_cached::use_server_cached, server_future::use_server_future};
|
||||||
|
|
||||||
#[cfg(feature = "axum")]
|
#[cfg(feature = "axum")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "axum")))]
|
||||||
|
@ -42,10 +44,6 @@ pub mod prelude {
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||||
pub use crate::render::{FullstackHTMLTemplate, SSRState};
|
pub use crate::render::{FullstackHTMLTemplate, SSRState};
|
||||||
|
|
||||||
#[cfg(feature = "router")]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "router")))]
|
|
||||||
pub use crate::router::FullstackRouterConfig;
|
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
#[cfg_attr(docsrs, doc(cfg(feature = "server")))]
|
||||||
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
|
pub use crate::serve_config::{ServeConfig, ServeConfigBuilder};
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
//! A shared pool of renderers for efficient server side rendering.
|
//! A shared pool of renderers for efficient server side rendering.
|
||||||
use crate::render::dioxus_core::NoOpMutations;
|
use crate::streaming::StreamingRenderer;
|
||||||
|
use dioxus_interpreter_js::INITIALIZE_STREAMING_JS;
|
||||||
use dioxus_ssr::{
|
use dioxus_ssr::{
|
||||||
incremental::{RenderFreshness, WrapBody},
|
incremental::{CachedRender, RenderFreshness},
|
||||||
Renderer,
|
Renderer,
|
||||||
};
|
};
|
||||||
use std::future::Future;
|
use futures_channel::mpsc::Sender;
|
||||||
use std::sync::Arc;
|
use futures_util::{Stream, StreamExt};
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use tokio::task::block_in_place;
|
use std::{collections::HashMap, future::Future};
|
||||||
|
use std::{fmt::Write, sync::Arc};
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
@ -20,11 +22,16 @@ where
|
||||||
{
|
{
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
tokio::task::spawn_blocking(move || {
|
use tokio_util::task::LocalPoolHandle;
|
||||||
tokio::runtime::Runtime::new()
|
static TASK_POOL: std::sync::OnceLock<LocalPoolHandle> = std::sync::OnceLock::new();
|
||||||
.expect("couldn't spawn runtime")
|
|
||||||
.block_on(f())
|
let pool = TASK_POOL.get_or_init(|| {
|
||||||
})
|
let threads = std::thread::available_parallelism()
|
||||||
|
.unwrap_or(std::num::NonZeroUsize::new(1).unwrap());
|
||||||
|
LocalPoolHandle::new(threads.into())
|
||||||
|
});
|
||||||
|
|
||||||
|
pool.spawn_pinned(f)
|
||||||
}
|
}
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
{
|
{
|
||||||
|
@ -32,134 +39,281 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SsrRendererPool {
|
struct SsrRendererPool {
|
||||||
Renderer(RwLock<Vec<Renderer>>),
|
renderers: RwLock<Vec<Renderer>>,
|
||||||
Incremental(RwLock<Vec<dioxus_ssr::incremental::IncrementalRenderer>>),
|
incremental_cache: Option<RwLock<dioxus_ssr::incremental::IncrementalRenderer>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SsrRendererPool {
|
impl SsrRendererPool {
|
||||||
async fn render_to(
|
fn new(
|
||||||
|
initial_size: usize,
|
||||||
|
incremental: Option<dioxus_ssr::incremental::IncrementalRendererConfig>,
|
||||||
|
) -> Self {
|
||||||
|
let renderers = RwLock::new((0..initial_size).map(|_| pre_renderer()).collect());
|
||||||
|
Self {
|
||||||
|
renderers,
|
||||||
|
incremental_cache: incremental.map(|cache| RwLock::new(cache.build())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_cached_route(
|
||||||
&self,
|
&self,
|
||||||
|
route: &str,
|
||||||
|
render_into: &mut Sender<Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
|
||||||
|
) -> Option<RenderFreshness> {
|
||||||
|
if let Some(incremental) = &self.incremental_cache {
|
||||||
|
if let Ok(mut incremental) = incremental.write() {
|
||||||
|
match incremental.get(route) {
|
||||||
|
Ok(Some(cached_render)) => {
|
||||||
|
let CachedRender {
|
||||||
|
freshness,
|
||||||
|
response,
|
||||||
|
..
|
||||||
|
} = cached_render;
|
||||||
|
_ = render_into.start_send(String::from_utf8(response.to_vec()).map_err(
|
||||||
|
|err| {
|
||||||
|
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
|
||||||
|
err,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
));
|
||||||
|
return Some(freshness);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!(
|
||||||
|
"Failed to get route \"{route}\" from incremental cache: {e}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn render_to(
|
||||||
|
self: Arc<Self>,
|
||||||
cfg: &ServeConfig,
|
cfg: &ServeConfig,
|
||||||
route: String,
|
route: String,
|
||||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
||||||
server_context: &DioxusServerContext,
|
server_context: &DioxusServerContext,
|
||||||
) -> Result<(RenderFreshness, String), dioxus_ssr::incremental::IncrementalRendererError> {
|
) -> Result<
|
||||||
let wrapper = FullstackHTMLTemplate {
|
(
|
||||||
cfg: cfg.clone(),
|
RenderFreshness,
|
||||||
server_context: server_context.clone(),
|
impl Stream<Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
|
||||||
};
|
),
|
||||||
match self {
|
dioxus_ssr::incremental::IncrementalRendererError,
|
||||||
Self::Renderer(pool) => {
|
> {
|
||||||
let server_context = server_context.clone();
|
struct ReceiverWithDrop {
|
||||||
let mut renderer = pool.write().unwrap().pop().unwrap_or_else(pre_renderer);
|
receiver: futures_channel::mpsc::Receiver<
|
||||||
|
Result<String, dioxus_ssr::incremental::IncrementalRendererError>,
|
||||||
|
>,
|
||||||
|
cancel_task: Option<tokio::task::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
impl Stream for ReceiverWithDrop {
|
||||||
|
type Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>;
|
||||||
|
|
||||||
spawn_platform(move || async move {
|
fn poll_next(
|
||||||
let mut vdom = virtual_dom_factory();
|
mut self: std::pin::Pin<&mut Self>,
|
||||||
let mut to = WriteBuffer { buffer: Vec::new() };
|
cx: &mut std::task::Context<'_>,
|
||||||
// poll the future, which may call server_context()
|
) -> std::task::Poll<Option<Self::Item>> {
|
||||||
tracing::info!("Rebuilding vdom");
|
self.receiver.poll_next_unpin(cx)
|
||||||
with_server_context(server_context.clone(), || {
|
|
||||||
block_in_place(|| vdom.rebuild(&mut NoOpMutations));
|
|
||||||
});
|
|
||||||
ProvideServerContext::new(vdom.wait_for_suspense(), server_context).await;
|
|
||||||
tracing::info!("Suspense resolved");
|
|
||||||
|
|
||||||
if let Err(err) = wrapper.render_before_body(&mut *to) {
|
|
||||||
let _ = tx.send(Err(err));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Err(err) = renderer.render_to(&mut to, &vdom) {
|
|
||||||
let _ = tx.send(Err(
|
|
||||||
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
|
|
||||||
));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Err(err) = wrapper.render_after_body(&mut *to) {
|
|
||||||
let _ = tx.send(Err(err));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
match String::from_utf8(to.buffer) {
|
|
||||||
Ok(html) => {
|
|
||||||
let _ = tx.send(Ok((renderer, RenderFreshness::now(None), html)));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
_ = tx.send(Err(
|
|
||||||
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
|
|
||||||
err,
|
|
||||||
)),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let (renderer, freshness, html) = rx.await.unwrap()?;
|
|
||||||
pool.write().unwrap().push(renderer);
|
|
||||||
Ok((freshness, html))
|
|
||||||
}
|
|
||||||
Self::Incremental(pool) => {
|
|
||||||
let mut renderer =
|
|
||||||
pool.write().unwrap().pop().unwrap_or_else(|| {
|
|
||||||
incremental_pre_renderer(cfg.incremental.as_ref().unwrap())
|
|
||||||
});
|
|
||||||
|
|
||||||
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
||||||
|
|
||||||
let server_context = server_context.clone();
|
|
||||||
spawn_platform(move || async move {
|
|
||||||
let mut to = WriteBuffer { buffer: Vec::new() };
|
|
||||||
match renderer
|
|
||||||
.render(
|
|
||||||
route,
|
|
||||||
virtual_dom_factory,
|
|
||||||
&mut *to,
|
|
||||||
|vdom| {
|
|
||||||
Box::pin(async move {
|
|
||||||
// poll the future, which may call server_context()
|
|
||||||
tracing::info!("Rebuilding vdom");
|
|
||||||
with_server_context(server_context.clone(), || {
|
|
||||||
block_in_place(|| vdom.rebuild(&mut NoOpMutations));
|
|
||||||
});
|
|
||||||
ProvideServerContext::new(
|
|
||||||
vdom.wait_for_suspense(),
|
|
||||||
server_context,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
tracing::info!("Suspense resolved");
|
|
||||||
})
|
|
||||||
},
|
|
||||||
&wrapper,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(freshness) => {
|
|
||||||
match String::from_utf8(to.buffer).map_err(|err| {
|
|
||||||
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(
|
|
||||||
err,
|
|
||||||
))
|
|
||||||
}) {
|
|
||||||
Ok(html) => {
|
|
||||||
let _ = tx.send(Ok((freshness, html)));
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let _ = tx.send(Err(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
let _ = tx.send(Err(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let (freshness, html) = rx.await.unwrap()?;
|
|
||||||
|
|
||||||
Ok((freshness, html))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When we drop the stream, we need to cancel the task that is feeding values to the stream
|
||||||
|
impl Drop for ReceiverWithDrop {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(cancel_task) = self.cancel_task.take() {
|
||||||
|
cancel_task.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (mut into, rx) = futures_channel::mpsc::channel::<
|
||||||
|
Result<String, dioxus_ssr::incremental::IncrementalRendererError>,
|
||||||
|
>(1000);
|
||||||
|
|
||||||
|
// before we even spawn anything, we can check synchronously if we have the route cached
|
||||||
|
if let Some(freshness) = self.check_cached_route(&route, &mut into) {
|
||||||
|
return Ok((
|
||||||
|
freshness,
|
||||||
|
ReceiverWithDrop {
|
||||||
|
receiver: rx,
|
||||||
|
cancel_task: None,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrapper = FullstackHTMLTemplate { cfg: cfg.clone() };
|
||||||
|
|
||||||
|
let server_context = server_context.clone();
|
||||||
|
let mut renderer = self
|
||||||
|
.renderers
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.pop()
|
||||||
|
.unwrap_or_else(pre_renderer);
|
||||||
|
|
||||||
|
let myself = self.clone();
|
||||||
|
|
||||||
|
let join_handle = spawn_platform(move || async move {
|
||||||
|
let mut virtual_dom = virtual_dom_factory();
|
||||||
|
|
||||||
|
let mut pre_body = String::new();
|
||||||
|
if let Err(err) = wrapper.render_before_body(&mut pre_body) {
|
||||||
|
_ = into.start_send(Err(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Err(err) = write!(&mut pre_body, "<script>{INITIALIZE_STREAMING_JS}</script>") {
|
||||||
|
_ = into.start_send(Err(
|
||||||
|
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err),
|
||||||
|
));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stream = Arc::new(StreamingRenderer::new(pre_body, into));
|
||||||
|
let scope_to_mount_mapping = Arc::new(RwLock::new(HashMap::new()));
|
||||||
|
|
||||||
|
renderer.pre_render = true;
|
||||||
|
{
|
||||||
|
let scope_to_mount_mapping = scope_to_mount_mapping.clone();
|
||||||
|
let stream = stream.clone();
|
||||||
|
renderer.set_render_components(move |renderer, to, vdom, scope| {
|
||||||
|
let is_suspense_boundary = vdom
|
||||||
|
.get_scope(scope)
|
||||||
|
.and_then(|s| SuspenseBoundaryProps::downcast_from_scope(s))
|
||||||
|
.filter(|s| s.suspended())
|
||||||
|
.is_some();
|
||||||
|
if is_suspense_boundary {
|
||||||
|
let mount = stream.render_placeholder(
|
||||||
|
|to| renderer.render_scope(to, vdom, scope),
|
||||||
|
&mut *to,
|
||||||
|
)?;
|
||||||
|
scope_to_mount_mapping.write().unwrap().insert(scope, mount);
|
||||||
|
} else {
|
||||||
|
renderer.render_scope(to, vdom, scope)?
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! throw_error {
|
||||||
|
($e:expr) => {
|
||||||
|
stream.close_with_error($e);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// poll the future, which may call server_context()
|
||||||
|
tracing::info!("Rebuilding vdom");
|
||||||
|
with_server_context(server_context.clone(), || virtual_dom.rebuild_in_place());
|
||||||
|
|
||||||
|
// Render the initial frame with loading placeholders
|
||||||
|
let mut initial_frame = renderer.render(&virtual_dom);
|
||||||
|
|
||||||
|
// Collect the initial server data from the root node. For most apps, no use_server_futures will be resolved initially, so this will be full on `None`s.
|
||||||
|
// Sending down those Nones are still important to tell the client not to run the use_server_futures that are already running on the backend
|
||||||
|
let resolved_data = serialize_server_data(&virtual_dom, ScopeId::ROOT);
|
||||||
|
initial_frame.push_str(&format!(
|
||||||
|
r#"<script>window.initial_dioxus_hydration_data="{resolved_data}";</script>"#,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Along with the initial frame, we render the html after the main element, but before the body tag closes. This should include the script that starts loading the wasm bundle.
|
||||||
|
if let Err(err) = wrapper.render_after_main(&mut initial_frame) {
|
||||||
|
throw_error!(err);
|
||||||
|
}
|
||||||
|
stream.render(initial_frame);
|
||||||
|
|
||||||
|
// After the initial render, we need to resolve suspense
|
||||||
|
while virtual_dom.suspended_tasks_remaining() {
|
||||||
|
ProvideServerContext::new(
|
||||||
|
virtual_dom.wait_for_suspense_work(),
|
||||||
|
server_context.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
let resolved_suspense_nodes = ProvideServerContext::new(
|
||||||
|
virtual_dom.render_suspense_immediate(),
|
||||||
|
server_context.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Just rerender the resolved nodes
|
||||||
|
for scope in resolved_suspense_nodes {
|
||||||
|
let mount = {
|
||||||
|
let mut lock = scope_to_mount_mapping.write().unwrap();
|
||||||
|
lock.remove(&scope).unwrap()
|
||||||
|
};
|
||||||
|
let mut resolved_chunk = String::new();
|
||||||
|
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
|
||||||
|
let render_suspense = |into: &mut String| {
|
||||||
|
renderer.reset_hydration();
|
||||||
|
renderer.render_scope(into, &virtual_dom, scope)
|
||||||
|
};
|
||||||
|
let resolved_data = serialize_server_data(&virtual_dom, scope);
|
||||||
|
if let Err(err) = stream.replace_placeholder(
|
||||||
|
mount,
|
||||||
|
render_suspense,
|
||||||
|
resolved_data,
|
||||||
|
&mut resolved_chunk,
|
||||||
|
) {
|
||||||
|
throw_error!(
|
||||||
|
dioxus_ssr::incremental::IncrementalRendererError::RenderError(err)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.render(resolved_chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tracing::info!("Suspense resolved");
|
||||||
|
|
||||||
|
// After suspense is done, we render the html after the body
|
||||||
|
let mut post_streaming = String::new();
|
||||||
|
|
||||||
|
if let Err(err) = wrapper.render_after_body(&mut post_streaming) {
|
||||||
|
throw_error!(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If incremental rendering is enabled, add the new render to the cache without the streaming bits
|
||||||
|
if let Some(incremental) = &self.incremental_cache {
|
||||||
|
let mut cached_render = String::new();
|
||||||
|
if let Err(err) = wrapper.render_before_body(&mut cached_render) {
|
||||||
|
throw_error!(err);
|
||||||
|
}
|
||||||
|
cached_render.push_str(&post_streaming);
|
||||||
|
|
||||||
|
if let Ok(mut incremental) = incremental.write() {
|
||||||
|
let _ = incremental.cache(route, cached_render);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.render(post_streaming);
|
||||||
|
|
||||||
|
renderer.reset_render_components();
|
||||||
|
myself.renderers.write().unwrap().push(renderer);
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
RenderFreshness::now(None),
|
||||||
|
ReceiverWithDrop {
|
||||||
|
receiver: rx,
|
||||||
|
cancel_task: Some(join_handle),
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn serialize_server_data(virtual_dom: &VirtualDom, scope: ScopeId) -> String {
|
||||||
|
// After we replace the placeholder in the dom with javascript, we need to send down the resolved data so that the client can hydrate the node
|
||||||
|
// Extract any data we serialized for hydration (from server futures)
|
||||||
|
let html_data =
|
||||||
|
crate::html_storage::HTMLData::extract_from_suspense_boundary(virtual_dom, scope);
|
||||||
|
|
||||||
|
// serialize the server state into a base64 string
|
||||||
|
html_data.serialized()
|
||||||
|
}
|
||||||
|
|
||||||
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
|
/// State used in server side rendering. This utilizes a pool of [`dioxus_ssr::Renderer`]s to cache static templates between renders.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SSRState {
|
pub struct SSRState {
|
||||||
|
@ -170,24 +324,8 @@ pub struct SSRState {
|
||||||
impl SSRState {
|
impl SSRState {
|
||||||
/// Create a new [`SSRState`].
|
/// Create a new [`SSRState`].
|
||||||
pub fn new(cfg: &ServeConfig) -> Self {
|
pub fn new(cfg: &ServeConfig) -> Self {
|
||||||
if cfg.incremental.is_some() {
|
|
||||||
return Self {
|
|
||||||
renderers: Arc::new(SsrRendererPool::Incremental(RwLock::new(vec![
|
|
||||||
incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
|
|
||||||
incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
|
|
||||||
incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
|
|
||||||
incremental_pre_renderer(cfg.incremental.as_ref().unwrap()),
|
|
||||||
]))),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
renderers: Arc::new(SsrRendererPool::Renderer(RwLock::new(vec![
|
renderers: Arc::new(SsrRendererPool::new(4, cfg.incremental.clone())),
|
||||||
pre_renderer(),
|
|
||||||
pre_renderer(),
|
|
||||||
pre_renderer(),
|
|
||||||
pre_renderer(),
|
|
||||||
]))),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -198,15 +336,17 @@ impl SSRState {
|
||||||
cfg: &'a ServeConfig,
|
cfg: &'a ServeConfig,
|
||||||
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
virtual_dom_factory: impl FnOnce() -> VirtualDom + Send + Sync + 'static,
|
||||||
server_context: &'a DioxusServerContext,
|
server_context: &'a DioxusServerContext,
|
||||||
) -> Result<RenderResponse, dioxus_ssr::incremental::IncrementalRendererError> {
|
) -> Result<
|
||||||
let ServeConfig { .. } = cfg;
|
(
|
||||||
|
RenderFreshness,
|
||||||
let (freshness, html) = self
|
impl Stream<Item = Result<String, dioxus_ssr::incremental::IncrementalRendererError>>,
|
||||||
.renderers
|
),
|
||||||
|
dioxus_ssr::incremental::IncrementalRendererError,
|
||||||
|
> {
|
||||||
|
self.renderers
|
||||||
|
.clone()
|
||||||
.render_to(cfg, route, virtual_dom_factory, server_context)
|
.render_to(cfg, route, virtual_dom_factory, server_context)
|
||||||
.await?;
|
.await
|
||||||
|
|
||||||
Ok(RenderResponse { html, freshness })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,93 +354,60 @@ impl SSRState {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct FullstackHTMLTemplate {
|
pub struct FullstackHTMLTemplate {
|
||||||
cfg: ServeConfig,
|
cfg: ServeConfig,
|
||||||
server_context: DioxusServerContext,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FullstackHTMLTemplate {
|
impl FullstackHTMLTemplate {
|
||||||
/// Create a new [`FullstackHTMLTemplate`].
|
/// Create a new [`FullstackHTMLTemplate`].
|
||||||
pub fn new(cfg: &ServeConfig, server_context: &DioxusServerContext) -> Self {
|
pub fn new(cfg: &ServeConfig) -> Self {
|
||||||
Self {
|
Self { cfg: cfg.clone() }
|
||||||
cfg: cfg.clone(),
|
|
||||||
server_context: server_context.clone(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl dioxus_ssr::incremental::WrapBody for FullstackHTMLTemplate {
|
impl FullstackHTMLTemplate {
|
||||||
fn render_before_body<R: std::io::Write>(
|
/// Render any content before the body of the page.
|
||||||
|
pub fn render_before_body<R: std::fmt::Write>(
|
||||||
&self,
|
&self,
|
||||||
to: &mut R,
|
to: &mut R,
|
||||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||||
let ServeConfig { index, .. } = &self.cfg;
|
let ServeConfig { index, .. } = &self.cfg;
|
||||||
|
|
||||||
to.write_all(index.pre_main.as_bytes())?;
|
to.write_str(&index.pre_main)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_after_body<R: std::io::Write>(
|
/// Render all content after the main element of the page.
|
||||||
|
pub fn render_after_main<R: std::fmt::Write>(
|
||||||
&self,
|
&self,
|
||||||
to: &mut R,
|
to: &mut R,
|
||||||
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||||
// serialize the server state
|
|
||||||
crate::html_storage::serialize::encode_in_element(
|
|
||||||
&*self.server_context.html_data().map_err(|_| {
|
|
||||||
dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new({
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct HTMLDataReadError;
|
|
||||||
|
|
||||||
impl std::fmt::Display for HTMLDataReadError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(
|
|
||||||
"Failed to read the server data to serialize it into the HTML",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for HTMLDataReadError {}
|
|
||||||
|
|
||||||
HTMLDataReadError
|
|
||||||
}))
|
|
||||||
})?,
|
|
||||||
to,
|
|
||||||
)
|
|
||||||
.map_err(|err| dioxus_ssr::incremental::IncrementalRendererError::Other(Box::new(err)))?;
|
|
||||||
|
|
||||||
#[cfg(all(debug_assertions, feature = "hot-reload"))]
|
#[cfg(all(debug_assertions, feature = "hot-reload"))]
|
||||||
{
|
{
|
||||||
// In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work
|
// In debug mode, we need to add a script to the page that will reload the page if the websocket disconnects to make full recompile hot reloads work
|
||||||
let disconnect_js = dioxus_hot_reload::RECONNECT_SCRIPT;
|
let disconnect_js = dioxus_hot_reload::RECONNECT_SCRIPT;
|
||||||
|
|
||||||
to.write_all(r#"<script>"#.as_bytes())?;
|
to.write_str(r#"<script>"#)?;
|
||||||
to.write_all(disconnect_js.as_bytes())?;
|
to.write_str(disconnect_js)?;
|
||||||
to.write_all(r#"</script>"#.as_bytes())?;
|
to.write_str(r#"</script>"#)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ServeConfig { index, .. } = &self.cfg;
|
let ServeConfig { index, .. } = &self.cfg;
|
||||||
|
|
||||||
to.write_all(index.post_main.as_bytes())?;
|
to.write_str(&index.post_main)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// A rendered response from the server.
|
/// Render all content after the body of the page.
|
||||||
#[derive(Debug)]
|
pub fn render_after_body<R: std::fmt::Write>(
|
||||||
pub struct RenderResponse {
|
&self,
|
||||||
pub(crate) html: String,
|
to: &mut R,
|
||||||
pub(crate) freshness: RenderFreshness,
|
) -> Result<(), dioxus_ssr::incremental::IncrementalRendererError> {
|
||||||
}
|
let ServeConfig { index, .. } = &self.cfg;
|
||||||
|
|
||||||
impl RenderResponse {
|
to.write_str(&index.after_closing_body_tag)?;
|
||||||
/// Get the rendered HTML.
|
|
||||||
pub fn html(&self) -> &str {
|
|
||||||
&self.html
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the freshness of the rendered HTML.
|
Ok(())
|
||||||
pub fn freshness(&self) -> RenderFreshness {
|
|
||||||
self.freshness
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,36 +416,3 @@ fn pre_renderer() -> Renderer {
|
||||||
renderer.pre_render = true;
|
renderer.pre_render = true;
|
||||||
renderer
|
renderer
|
||||||
}
|
}
|
||||||
|
|
||||||
fn incremental_pre_renderer(
|
|
||||||
cfg: &IncrementalRendererConfig,
|
|
||||||
) -> dioxus_ssr::incremental::IncrementalRenderer {
|
|
||||||
let mut renderer = cfg.clone().build();
|
|
||||||
renderer.renderer_mut().pre_render = true;
|
|
||||||
renderer
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WriteBuffer {
|
|
||||||
buffer: Vec<u8>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Write for WriteBuffer {
|
|
||||||
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
|
||||||
self.buffer.extend_from_slice(s.as_bytes());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::Deref for WriteBuffer {
|
|
||||||
type Target = Vec<u8>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::ops::DerefMut for WriteBuffer {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.buffer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,8 +12,7 @@ pub struct ServeConfigBuilder {
|
||||||
pub(crate) index_html: Option<String>,
|
pub(crate) index_html: Option<String>,
|
||||||
pub(crate) index_path: Option<PathBuf>,
|
pub(crate) index_path: Option<PathBuf>,
|
||||||
pub(crate) assets_path: Option<PathBuf>,
|
pub(crate) assets_path: Option<PathBuf>,
|
||||||
pub(crate) incremental:
|
pub(crate) incremental: Option<dioxus_ssr::incremental::IncrementalRendererConfig>,
|
||||||
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServeConfigBuilder {
|
impl ServeConfigBuilder {
|
||||||
|
@ -30,7 +29,7 @@ impl ServeConfigBuilder {
|
||||||
|
|
||||||
/// Enable incremental static generation
|
/// Enable incremental static generation
|
||||||
pub fn incremental(mut self, cfg: dioxus_ssr::incremental::IncrementalRendererConfig) -> Self {
|
pub fn incremental(mut self, cfg: dioxus_ssr::incremental::IncrementalRendererConfig) -> Self {
|
||||||
self.incremental = Some(std::sync::Arc::new(cfg));
|
self.incremental = Some(cfg);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,9 +107,15 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
|
||||||
post_main.1.to_string(),
|
post_main.1.to_string(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let (post_main, after_closing_body_tag) =
|
||||||
|
post_main.split_once("</body>").unwrap_or_else(|| {
|
||||||
|
panic!("Failed to find closing </body> tag after id=\"{root_id}\" in index.html.")
|
||||||
|
});
|
||||||
|
|
||||||
IndexHtml {
|
IndexHtml {
|
||||||
pre_main,
|
pre_main,
|
||||||
post_main,
|
post_main: post_main.to_string(),
|
||||||
|
after_closing_body_tag: "</body>".to_string() + after_closing_body_tag,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +123,7 @@ fn load_index_html(contents: String, root_id: &'static str) -> IndexHtml {
|
||||||
pub(crate) struct IndexHtml {
|
pub(crate) struct IndexHtml {
|
||||||
pub(crate) pre_main: String,
|
pub(crate) pre_main: String,
|
||||||
pub(crate) post_main: String,
|
pub(crate) post_main: String,
|
||||||
|
pub(crate) after_closing_body_tag: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
|
/// Used to configure how to serve a Dioxus application. It contains information about how to serve static assets, and what content to render with [`dioxus-ssr`].
|
||||||
|
@ -127,8 +133,7 @@ pub struct ServeConfig {
|
||||||
pub(crate) index: IndexHtml,
|
pub(crate) index: IndexHtml,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(crate) assets_path: PathBuf,
|
pub(crate) assets_path: PathBuf,
|
||||||
pub(crate) incremental:
|
pub(crate) incremental: Option<dioxus_ssr::incremental::IncrementalRendererConfig>,
|
||||||
Option<std::sync::Arc<dioxus_ssr::incremental::IncrementalRendererConfig>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ServeConfig {
|
impl Default for ServeConfig {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
use crate::html_storage::HTMLData;
|
use parking_lot::RwLock;
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::RwLock;
|
|
||||||
|
|
||||||
type SendSyncAnyMap =
|
type SendSyncAnyMap =
|
||||||
std::collections::HashMap<std::any::TypeId, Box<dyn Any + Send + Sync + 'static>>;
|
std::collections::HashMap<std::any::TypeId, Box<dyn Any + Send + Sync + 'static>>;
|
||||||
|
@ -13,44 +12,50 @@ type SendSyncAnyMap =
|
||||||
/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope.
|
/// You should not construct this directly inside components. Instead use the `HasServerContext` trait to get the server context from the scope.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DioxusServerContext {
|
pub struct DioxusServerContext {
|
||||||
shared_context: std::sync::Arc<std::sync::RwLock<SendSyncAnyMap>>,
|
shared_context: std::sync::Arc<RwLock<SendSyncAnyMap>>,
|
||||||
response_parts: std::sync::Arc<std::sync::RwLock<http::response::Parts>>,
|
response_parts: std::sync::Arc<RwLock<http::response::Parts>>,
|
||||||
pub(crate) parts: Arc<tokio::sync::RwLock<http::request::Parts>>,
|
pub(crate) parts: Arc<RwLock<http::request::Parts>>,
|
||||||
html_data: Arc<RwLock<HTMLData>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::derivable_impls)]
|
#[allow(clippy::derivable_impls)]
|
||||||
impl Default for DioxusServerContext {
|
impl Default for DioxusServerContext {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
shared_context: std::sync::Arc::new(std::sync::RwLock::new(HashMap::new())),
|
shared_context: std::sync::Arc::new(RwLock::new(HashMap::new())),
|
||||||
response_parts: std::sync::Arc::new(RwLock::new(
|
response_parts: std::sync::Arc::new(RwLock::new(
|
||||||
http::response::Response::new(()).into_parts().0,
|
http::response::Response::new(()).into_parts().0,
|
||||||
)),
|
)),
|
||||||
parts: std::sync::Arc::new(tokio::sync::RwLock::new(
|
parts: std::sync::Arc::new(RwLock::new(http::request::Request::new(()).into_parts().0)),
|
||||||
http::request::Request::new(()).into_parts().0,
|
|
||||||
)),
|
|
||||||
html_data: Arc::new(RwLock::new(HTMLData::default())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mod server_fn_impl {
|
mod server_fn_impl {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use parking_lot::{RwLockReadGuard, RwLockWriteGuard};
|
||||||
use std::any::{Any, TypeId};
|
use std::any::{Any, TypeId};
|
||||||
use std::sync::LockResult;
|
|
||||||
use std::sync::{PoisonError, RwLockReadGuard, RwLockWriteGuard};
|
|
||||||
|
|
||||||
impl DioxusServerContext {
|
impl DioxusServerContext {
|
||||||
/// Create a new server context from a request
|
/// Create a new server context from a request
|
||||||
pub fn new(parts: impl Into<Arc<tokio::sync::RwLock<http::request::Parts>>>) -> Self {
|
pub fn new(parts: http::request::Parts) -> Self {
|
||||||
Self {
|
Self {
|
||||||
parts: parts.into(),
|
parts: Arc::new(RwLock::new(parts)),
|
||||||
|
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
|
||||||
|
response_parts: std::sync::Arc::new(RwLock::new(
|
||||||
|
http::response::Response::new(()).into_parts().0,
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a server context from a shared parts
|
||||||
|
#[allow(unused)]
|
||||||
|
pub(crate) fn from_shared_parts(parts: Arc<RwLock<http::request::Parts>>) -> Self {
|
||||||
|
Self {
|
||||||
|
parts,
|
||||||
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
|
shared_context: Arc::new(RwLock::new(SendSyncAnyMap::new())),
|
||||||
response_parts: std::sync::Arc::new(RwLock::new(
|
response_parts: std::sync::Arc::new(RwLock::new(
|
||||||
http::response::Response::new(()).into_parts().0,
|
http::response::Response::new(()).into_parts().0,
|
||||||
)),
|
)),
|
||||||
html_data: Arc::new(RwLock::new(HTMLData::default())),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,79 +63,46 @@ mod server_fn_impl {
|
||||||
pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
|
pub fn get<T: Any + Send + Sync + Clone + 'static>(&self) -> Option<T> {
|
||||||
self.shared_context
|
self.shared_context
|
||||||
.read()
|
.read()
|
||||||
.ok()?
|
|
||||||
.get(&TypeId::of::<T>())
|
.get(&TypeId::of::<T>())
|
||||||
.map(|v| v.downcast_ref::<T>().unwrap().clone())
|
.map(|v| v.downcast_ref::<T>().unwrap().clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a value into the shared server context
|
/// Insert a value into the shared server context
|
||||||
pub fn insert<T: Any + Send + Sync + 'static>(
|
pub fn insert<T: Any + Send + Sync + 'static>(&self, value: T) {
|
||||||
&self,
|
|
||||||
value: T,
|
|
||||||
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
|
|
||||||
self.shared_context
|
self.shared_context
|
||||||
.write()
|
.write()
|
||||||
.map(|mut map| map.insert(TypeId::of::<T>(), Box::new(value)))
|
.insert(TypeId::of::<T>(), Box::new(value));
|
||||||
.map(|_| ())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a Boxed `Any` value into the shared server context
|
/// Insert a Boxed `Any` value into the shared server context
|
||||||
pub fn insert_any(
|
pub fn insert_any(&self, value: Box<dyn Any + Send + Sync>) {
|
||||||
&self,
|
|
||||||
value: Box<dyn Any + Send + Sync>,
|
|
||||||
) -> Result<(), PoisonError<RwLockWriteGuard<'_, SendSyncAnyMap>>> {
|
|
||||||
self.shared_context
|
self.shared_context
|
||||||
.write()
|
.write()
|
||||||
.map(|mut map| map.insert((*value).type_id(), value))
|
.insert((*value).type_id(), value);
|
||||||
.map(|_| ())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the response parts from the server context
|
/// Get the response parts from the server context
|
||||||
pub fn response_parts(&self) -> LockResult<RwLockReadGuard<'_, http::response::Parts>> {
|
pub fn response_parts(&self) -> RwLockReadGuard<'_, http::response::Parts> {
|
||||||
self.response_parts.read()
|
self.response_parts.read()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the response parts from the server context
|
/// Get the response parts from the server context
|
||||||
pub fn response_parts_mut(
|
pub fn response_parts_mut(&self) -> RwLockWriteGuard<'_, http::response::Parts> {
|
||||||
&self,
|
|
||||||
) -> LockResult<RwLockWriteGuard<'_, http::response::Parts>> {
|
|
||||||
self.response_parts.write()
|
self.response_parts.write()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the request that triggered:
|
/// Get the request that triggered:
|
||||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
/// - The initial SSR render if called from a ScopeState or ServerFn
|
||||||
/// - The server function to be called if called from a server function after the initial render
|
/// - The server function to be called if called from a server function after the initial render
|
||||||
pub async fn request_parts(
|
pub fn request_parts(&self) -> parking_lot::RwLockReadGuard<'_, http::request::Parts> {
|
||||||
&self,
|
self.parts.read()
|
||||||
) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
|
|
||||||
self.parts.read().await
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the request that triggered:
|
/// Get the request that triggered:
|
||||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
/// - The initial SSR render if called from a ScopeState or ServerFn
|
||||||
/// - The server function to be called if called from a server function after the initial render
|
/// - The server function to be called if called from a server function after the initial render
|
||||||
pub fn request_parts_blocking(
|
pub fn request_parts_mut(&self) -> parking_lot::RwLockWriteGuard<'_, http::request::Parts> {
|
||||||
&self,
|
self.parts.write()
|
||||||
) -> tokio::sync::RwLockReadGuard<'_, http::request::Parts> {
|
|
||||||
self.parts.blocking_read()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the request that triggered:
|
|
||||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
|
||||||
/// - The server function to be called if called from a server function after the initial render
|
|
||||||
pub async fn request_parts_mut(
|
|
||||||
&self,
|
|
||||||
) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
|
|
||||||
self.parts.write().await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the request that triggered:
|
|
||||||
/// - The initial SSR render if called from a ScopeState or ServerFn
|
|
||||||
/// - The server function to be called if called from a server function after the initial render
|
|
||||||
pub fn request_parts_mut_blocking(
|
|
||||||
&self,
|
|
||||||
) -> tokio::sync::RwLockWriteGuard<'_, http::request::Parts> {
|
|
||||||
self.parts.blocking_write()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract some part from the request
|
/// Extract some part from the request
|
||||||
|
@ -139,21 +111,6 @@ mod server_fn_impl {
|
||||||
) -> Result<T, R> {
|
) -> Result<T, R> {
|
||||||
T::from_request(self).await
|
T::from_request(self).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert some data into the html data store
|
|
||||||
pub(crate) fn push_html_data<T: serde::Serialize>(
|
|
||||||
&self,
|
|
||||||
value: &T,
|
|
||||||
) -> Result<(), PoisonError<RwLockWriteGuard<'_, HTMLData>>> {
|
|
||||||
self.html_data.write().map(|mut map| {
|
|
||||||
map.push(value);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the html data store
|
|
||||||
pub(crate) fn html_data(&self) -> LockResult<RwLockReadGuard<'_, HTMLData>> {
|
|
||||||
self.html_data.read()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +146,7 @@ pub fn with_server_context<O>(context: DioxusServerContext, f: impl FnOnce() ->
|
||||||
/// A future that provides the server context to the inner future
|
/// A future that provides the server context to the inner future
|
||||||
#[pin_project::pin_project]
|
#[pin_project::pin_project]
|
||||||
pub struct ProvideServerContext<F: std::future::Future> {
|
pub struct ProvideServerContext<F: std::future::Future> {
|
||||||
context: Option<DioxusServerContext>,
|
context: DioxusServerContext,
|
||||||
#[pin]
|
#[pin]
|
||||||
f: F,
|
f: F,
|
||||||
}
|
}
|
||||||
|
@ -197,10 +154,7 @@ pub struct ProvideServerContext<F: std::future::Future> {
|
||||||
impl<F: std::future::Future> ProvideServerContext<F> {
|
impl<F: std::future::Future> ProvideServerContext<F> {
|
||||||
/// Create a new future that provides the server context to the inner future
|
/// Create a new future that provides the server context to the inner future
|
||||||
pub fn new(f: F, context: DioxusServerContext) -> Self {
|
pub fn new(f: F, context: DioxusServerContext) -> Self {
|
||||||
Self {
|
Self { f, context }
|
||||||
context: Some(context),
|
|
||||||
f,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,10 +166,8 @@ impl<F: std::future::Future> std::future::Future for ProvideServerContext<F> {
|
||||||
cx: &mut std::task::Context<'_>,
|
cx: &mut std::task::Context<'_>,
|
||||||
) -> std::task::Poll<Self::Output> {
|
) -> std::task::Poll<Self::Output> {
|
||||||
let this = self.project();
|
let this = self.project();
|
||||||
let context = this.context.take().unwrap();
|
let context = this.context.clone();
|
||||||
let result = with_server_context(context.clone(), || this.f.poll(cx));
|
with_server_context(context, || this.f.poll(cx))
|
||||||
*this.context = Some(context);
|
|
||||||
result
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -302,7 +254,9 @@ impl<
|
||||||
{
|
{
|
||||||
type Rejection = R;
|
type Rejection = R;
|
||||||
|
|
||||||
|
#[allow(clippy::all)]
|
||||||
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
|
async fn from_request(req: &DioxusServerContext) -> Result<Self, Self::Rejection> {
|
||||||
Ok(I::from_request_parts(&mut *req.request_parts_mut().await, &()).await?)
|
let mut lock = req.request_parts_mut();
|
||||||
|
I::from_request_parts(&mut lock, &()).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
144
packages/fullstack/src/streaming.rs
Normal file
144
packages/fullstack/src/streaming.rs
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
//! There are two common ways to render suspense:
|
||||||
|
//! 1. Stream the HTML in order - this will work even if javascript is disabled, but if there is something slow at the top of your page, and fast at the bottom, nothing will render until the slow part is done
|
||||||
|
//! 2. Render placeholders and stream the HTML out of order - this will only work if javascript is enabled. This lets you render any parts of your page that resolve quickly, and then render the rest of the page as it becomes available
|
||||||
|
//!
|
||||||
|
//! Dioxus currently uses a the second out of order streaming approach which requires javascript. The rendering structure is as follows:
|
||||||
|
//! ```html
|
||||||
|
//! // Initial content is sent down with placeholders
|
||||||
|
//! <div>
|
||||||
|
//! Header
|
||||||
|
//! <div class="flex flex-col">
|
||||||
|
//! // If we reach a suspense placeholder that may be replaced later, we insert a template node with a unique id to replace later
|
||||||
|
//! <div>Loading user info...</div>
|
||||||
|
//! </div>
|
||||||
|
//! Footer
|
||||||
|
//! </div>
|
||||||
|
//! // After the initial render is done, we insert divs that are hidden with new content.
|
||||||
|
//! // We use divs instead of templates for better SEO
|
||||||
|
//! <script>
|
||||||
|
//! // Code to hook up hydration replacement
|
||||||
|
//! </script>
|
||||||
|
//! <div hidden id="ds-1-r">
|
||||||
|
//! <div>Final HTML</div>
|
||||||
|
//! </div>
|
||||||
|
//! <script>
|
||||||
|
//! window.dx_hydrate(2, "suspenseboundarydata");
|
||||||
|
//! </script>
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
use futures_channel::mpsc::Sender;
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
fmt::{Display, Write},
|
||||||
|
sync::{Arc, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Sections are identified by a unique id based on the suspense path. We only track the path of suspense boundaries because the client may render different components than the server.
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct MountPath {
|
||||||
|
parent: Option<Arc<MountPath>>,
|
||||||
|
id: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MountPath {
|
||||||
|
fn child(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
parent: Some(Arc::new(self.clone())),
|
||||||
|
id: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for MountPath {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if let Some(parent) = &self.parent {
|
||||||
|
write!(f, "{},", parent)?;
|
||||||
|
}
|
||||||
|
write!(f, "{}", self.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct StreamingRenderer<E = std::convert::Infallible> {
|
||||||
|
channel: RwLock<Sender<Result<String, E>>>,
|
||||||
|
current_path: RwLock<MountPath>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> StreamingRenderer<E> {
|
||||||
|
/// Create a new streaming renderer with the given head that renders into a channel
|
||||||
|
pub(crate) fn new(
|
||||||
|
before_body: impl Display,
|
||||||
|
mut render_into: Sender<Result<String, E>>,
|
||||||
|
) -> Self {
|
||||||
|
let start_html = before_body.to_string();
|
||||||
|
_ = render_into.start_send(Ok(start_html));
|
||||||
|
|
||||||
|
Self {
|
||||||
|
channel: render_into.into(),
|
||||||
|
current_path: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a new chunk of html that will never change
|
||||||
|
pub(crate) fn render(&self, html: impl Display) {
|
||||||
|
_ = self
|
||||||
|
.channel
|
||||||
|
.write()
|
||||||
|
.unwrap()
|
||||||
|
.start_send(Ok(html.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a new chunk of html that may change
|
||||||
|
pub(crate) fn render_placeholder<W: Write + ?Sized>(
|
||||||
|
&self,
|
||||||
|
html: impl FnOnce(&mut W) -> std::fmt::Result,
|
||||||
|
into: &mut W,
|
||||||
|
) -> Result<Mount, std::fmt::Error> {
|
||||||
|
let id = self.current_path.read().unwrap().clone();
|
||||||
|
// Increment the id for the next placeholder
|
||||||
|
self.current_path.write().unwrap().id += 1;
|
||||||
|
// While we are inside the placeholder, set the suspense path to the suspense boundary that we are rendering
|
||||||
|
let old_path = std::mem::replace(&mut *self.current_path.write().unwrap(), id.child());
|
||||||
|
html(into)?;
|
||||||
|
// Restore the old path
|
||||||
|
*self.current_path.write().unwrap() = old_path;
|
||||||
|
Ok(Mount { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace a placeholder that was rendered previously
|
||||||
|
pub(crate) fn replace_placeholder<W: Write + ?Sized>(
|
||||||
|
&self,
|
||||||
|
id: Mount,
|
||||||
|
html: impl FnOnce(&mut W) -> std::fmt::Result,
|
||||||
|
data: impl Display,
|
||||||
|
into: &mut W,
|
||||||
|
) -> std::fmt::Result {
|
||||||
|
// Then replace the suspense placeholder with the new content
|
||||||
|
write!(into, r#"<div id="ds-{id}-r" hidden>"#)?;
|
||||||
|
// While we are inside the placeholder, set the suspense path to the suspense boundary that we are rendering
|
||||||
|
let old_path = std::mem::replace(&mut *self.current_path.write().unwrap(), id.id.child());
|
||||||
|
html(into)?;
|
||||||
|
// Restore the old path
|
||||||
|
*self.current_path.write().unwrap() = old_path;
|
||||||
|
write!(
|
||||||
|
into,
|
||||||
|
r#"</div><script>window.dx_hydrate([{id}], "{data}")</script>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Close the stream with an error
|
||||||
|
pub(crate) fn close_with_error(&self, error: E) {
|
||||||
|
_ = self.channel.write().unwrap().start_send(Err(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mounted placeholder in the dom that may change in the future
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct Mount {
|
||||||
|
id: MountPath,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Mount {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "{}", self.id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,9 +18,7 @@ dioxus-core = { workspace = true }
|
||||||
dioxus-signals = { workspace = true }
|
dioxus-signals = { workspace = true }
|
||||||
futures-channel = { workspace = true }
|
futures-channel = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
|
||||||
slab = { workspace = true }
|
slab = { workspace = true }
|
||||||
dioxus-debug-cell = "0.1.1"
|
|
||||||
futures-util = { workspace = true}
|
futures-util = { workspace = true}
|
||||||
generational-box.workspace = true
|
generational-box.workspace = true
|
||||||
rustversion = "1.0.17"
|
rustversion = "1.0.17"
|
||||||
|
|
|
@ -26,7 +26,7 @@
|
||||||
/// });
|
/// });
|
||||||
/// };
|
/// };
|
||||||
/// # handle_thing(());
|
/// # handle_thing(());
|
||||||
/// # None }
|
/// # VNode::empty() }
|
||||||
/// ```
|
/// ```
|
||||||
macro_rules! to_owned {
|
macro_rules! to_owned {
|
||||||
// Rule matching simple symbols without a path
|
// Rule matching simple symbols without a path
|
||||||
|
|
|
@ -149,8 +149,7 @@ pub enum UseResourceState {
|
||||||
impl<T> Resource<T> {
|
impl<T> Resource<T> {
|
||||||
/// Restart the resource's future.
|
/// Restart the resource's future.
|
||||||
///
|
///
|
||||||
/// Will not cancel the previous future, but will ignore any values that it
|
/// This will cancel the current future and start a new one.
|
||||||
/// generates.
|
|
||||||
///
|
///
|
||||||
/// ## Example
|
/// ## Example
|
||||||
/// ```rust, no_run
|
/// ```rust, no_run
|
||||||
|
@ -407,6 +406,21 @@ impl<T> Resource<T> {
|
||||||
pub fn value(&self) -> ReadOnlySignal<Option<T>> {
|
pub fn value(&self) -> ReadOnlySignal<Option<T>> {
|
||||||
self.value.into()
|
self.value.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Suspend the resource's future and only continue rendering when the future is ready
|
||||||
|
pub fn suspend(&self) -> std::result::Result<MappedSignal<T>, RenderError> {
|
||||||
|
match self.state.cloned() {
|
||||||
|
UseResourceState::Stopped | UseResourceState::Paused | UseResourceState::Pending => {
|
||||||
|
let task = self.task();
|
||||||
|
if task.paused() {
|
||||||
|
Ok(self.value.map(|v| v.as_ref().unwrap()))
|
||||||
|
} else {
|
||||||
|
Err(RenderError::Suspended(SuspendedFuture::new(task)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => Ok(self.value.map(|v| v.as_ref().unwrap())),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> From<Resource<T>> for ReadOnlySignal<Option<T>> {
|
impl<T> From<Resource<T>> for ReadOnlySignal<Option<T>> {
|
||||||
|
|
|
@ -34,9 +34,9 @@ tracing.workspace = true
|
||||||
tokio = { workspace = true, features = ["full"] }
|
tokio = { workspace = true, features = ["full"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["dep:dioxus-html"]
|
default = ["dioxus-html"]
|
||||||
custom_file_watcher = ["dep:ignore", "dep:chrono", "dep:notify", "dep:execute", "dep:once_cell", "dep:ignore"]
|
custom_file_watcher = ["dep:ignore", "dep:chrono", "dep:notify", "dep:execute", "dep:once_cell", "dep:ignore"]
|
||||||
file_watcher = ["custom_file_watcher", "dioxus-html/hot-reload-context"]
|
file_watcher = ["custom_file_watcher", "dioxus-html/hot-reload-context", "dioxus-rsx/hot_reload"]
|
||||||
serve = ["dep:axum", "dep:tokio-stream", "dep:futures-util", "dep:tokio", "file_watcher"]
|
serve = ["dep:axum", "dep:tokio-stream", "dep:futures-util", "dep:tokio", "file_watcher"]
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|
|
@ -31,8 +31,6 @@
|
||||||
var ws = new WebSocket(url);
|
var ws = new WebSocket(url);
|
||||||
|
|
||||||
ws.onmessage = (ev) => {
|
ws.onmessage = (ev) => {
|
||||||
console.log("Received message: ", ev, ev.data);
|
|
||||||
|
|
||||||
if (ev.data == "reload") {
|
if (ev.data == "reload") {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,16 +11,17 @@ keywords = ["dom", "ui", "gui", "react"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dioxus-core = { workspace = true }
|
dioxus-core = { workspace = true }
|
||||||
dioxus-rsx = { workspace = true, features = ["hot_reload"], optional = true }
|
dioxus-rsx = { workspace = true, optional = true }
|
||||||
dioxus-html-internal-macro = { workspace = true }
|
dioxus-html-internal-macro = { workspace = true }
|
||||||
generational-box = { workspace = true }
|
generational-box = { workspace = true }
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
serde_repr = { version = "0.1", optional = true }
|
serde_repr = { version = "0.1", optional = true }
|
||||||
wasm-bindgen = { workspace = true, optional = true }
|
wasm-bindgen = { workspace = true, optional = true }
|
||||||
|
js-sys = { version = "0.3.56", optional = true }
|
||||||
euclid = "0.22.7"
|
euclid = "0.22.7"
|
||||||
enumset = "1.1.2"
|
enumset = "1.1.2"
|
||||||
keyboard-types = "0.7"
|
keyboard-types = { version = "0.7", default-features = false }
|
||||||
async-trait = "0.1.58"
|
async-trait = { version = "0.1.58", optional = true }
|
||||||
serde-value = { version = "0.7.0", optional = true }
|
serde-value = { version = "0.7.0", optional = true }
|
||||||
tokio = { workspace = true, features = ["fs", "io-util"], optional = true }
|
tokio = { workspace = true, features = ["fs", "io-util"], optional = true }
|
||||||
rfd = { version = "0.14", optional = true }
|
rfd = { version = "0.14", optional = true }
|
||||||
|
@ -39,6 +40,7 @@ features = [
|
||||||
"MouseEvent",
|
"MouseEvent",
|
||||||
"DragEvent",
|
"DragEvent",
|
||||||
"InputEvent",
|
"InputEvent",
|
||||||
|
"HtmlInputElement",
|
||||||
"ClipboardEvent",
|
"ClipboardEvent",
|
||||||
"KeyboardEvent",
|
"KeyboardEvent",
|
||||||
"WheelEvent",
|
"WheelEvent",
|
||||||
|
@ -56,7 +58,7 @@ dioxus-web = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["time"] }
|
tokio = { workspace = true, features = ["time"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["serialize", "mounted", "eval"]
|
default = ["serialize", "mounted", "eval", "file-engine"]
|
||||||
serialize = [
|
serialize = [
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
"dep:serde_json",
|
"dep:serde_json",
|
||||||
|
@ -78,9 +80,16 @@ eval = [
|
||||||
"dep:serde",
|
"dep:serde",
|
||||||
"dep:serde_json"
|
"dep:serde_json"
|
||||||
]
|
]
|
||||||
|
file-engine = [
|
||||||
|
"dep:async-trait",
|
||||||
|
"dep:js-sys",
|
||||||
|
"web-sys?/File",
|
||||||
|
"web-sys?/FileList",
|
||||||
|
"web-sys?/FileReader"
|
||||||
|
]
|
||||||
wasm-bind = ["dep:web-sys", "dep:wasm-bindgen"]
|
wasm-bind = ["dep:web-sys", "dep:wasm-bindgen"]
|
||||||
native-bind = ["dep:tokio"]
|
native-bind = ["dep:tokio", "file-engine"]
|
||||||
hot-reload-context = ["dep:dioxus-rsx"]
|
hot-reload-context = ["dep:dioxus-rsx", "dioxus-rsx/hot_reload_traits"]
|
||||||
html-to-rsx = []
|
html-to-rsx = []
|
||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::file_data::{FileEngine, HasFileData};
|
|
||||||
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
|
use crate::geometry::{ClientPoint, Coordinates, ElementPoint, PagePoint, ScreenPoint};
|
||||||
use crate::input_data::{MouseButton, MouseButtonSet};
|
use crate::input_data::{MouseButton, MouseButtonSet};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
|
@ -58,8 +57,9 @@ impl DragData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasFileData for DragData {
|
impl crate::HasFileData for DragData {
|
||||||
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
|
#[cfg(feature = "file-engine")]
|
||||||
|
fn files(&self) -> Option<std::sync::Arc<dyn crate::file_data::FileEngine>> {
|
||||||
self.inner.files()
|
self.inner.files()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,16 +111,18 @@ impl PointerInteraction for DragData {
|
||||||
pub struct SerializedDragData {
|
pub struct SerializedDragData {
|
||||||
pub mouse: crate::point_interaction::SerializedPointInteraction,
|
pub mouse: crate::point_interaction::SerializedPointInteraction,
|
||||||
|
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
files: Option<crate::file_data::SerializedFileEngine>,
|
files: Option<crate::file_data::SerializedFileEngine>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
impl SerializedDragData {
|
impl SerializedDragData {
|
||||||
fn new(drag: &DragData, files: Option<crate::file_data::SerializedFileEngine>) -> Self {
|
fn new(drag: &DragData) -> Self {
|
||||||
Self {
|
Self {
|
||||||
mouse: crate::point_interaction::SerializedPointInteraction::from(drag),
|
mouse: crate::point_interaction::SerializedPointInteraction::from(drag),
|
||||||
files,
|
#[cfg(feature = "file-engine")]
|
||||||
|
files: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -133,8 +135,9 @@ impl HasDragData for SerializedDragData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
impl HasFileData for SerializedDragData {
|
impl crate::file_data::HasFileData for SerializedDragData {
|
||||||
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
|
#[cfg(feature = "file-engine")]
|
||||||
|
fn files(&self) -> Option<std::sync::Arc<dyn crate::file_data::FileEngine>> {
|
||||||
self.files
|
self.files
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|files| std::sync::Arc::new(files.clone()) as _)
|
.map(|files| std::sync::Arc::new(files.clone()) as _)
|
||||||
|
@ -195,7 +198,7 @@ impl PointerInteraction for SerializedDragData {
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
impl serde::Serialize for DragData {
|
impl serde::Serialize for DragData {
|
||||||
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||||
SerializedDragData::new(self, None).serialize(serializer)
|
SerializedDragData::new(self).serialize(serializer)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +213,7 @@ impl<'de> serde::Deserialize<'de> for DragData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A trait for any object that has the data for a drag event
|
/// A trait for any object that has the data for a drag event
|
||||||
pub trait HasDragData: HasMouseData + HasFileData {
|
pub trait HasDragData: HasMouseData + crate::HasFileData {
|
||||||
/// return self as Any
|
/// return self as Any
|
||||||
fn as_any(&self) -> &dyn std::any::Any;
|
fn as_any(&self) -> &dyn std::any::Any;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use crate::file_data::FileEngine;
|
|
||||||
use crate::file_data::HasFileData;
|
use crate::file_data::HasFileData;
|
||||||
use std::{collections::HashMap, fmt::Debug, ops::Deref};
|
use std::{collections::HashMap, fmt::Debug, ops::Deref};
|
||||||
|
|
||||||
|
@ -106,7 +105,8 @@ impl FormData {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the files of the form event
|
/// Get the files of the form event
|
||||||
pub fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
|
#[cfg(feature = "file-engine")]
|
||||||
|
pub fn files(&self) -> Option<std::sync::Arc<dyn crate::file_data::FileEngine>> {
|
||||||
self.inner.files()
|
self.inner.files()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,6 +176,7 @@ pub struct SerializedFormData {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
valid: bool,
|
valid: bool,
|
||||||
|
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
files: Option<crate::file_data::SerializedFileEngine>,
|
files: Option<crate::file_data::SerializedFileEngine>,
|
||||||
}
|
}
|
||||||
|
@ -183,39 +184,46 @@ pub struct SerializedFormData {
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
impl SerializedFormData {
|
impl SerializedFormData {
|
||||||
/// Create a new serialized form data object
|
/// Create a new serialized form data object
|
||||||
pub fn new(
|
pub fn new(value: String, values: HashMap<String, FormValue>) -> Self {
|
||||||
value: String,
|
|
||||||
values: HashMap<String, FormValue>,
|
|
||||||
files: Option<crate::file_data::SerializedFileEngine>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
value,
|
value,
|
||||||
values,
|
values,
|
||||||
files,
|
|
||||||
valid: true,
|
valid: true,
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
|
files: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
|
/// Add files to the serialized form data object
|
||||||
|
pub fn with_files(mut self, files: crate::file_data::SerializedFileEngine) -> Self {
|
||||||
|
self.files = Some(files);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new serialized form data object from a traditional form data object
|
/// Create a new serialized form data object from a traditional form data object
|
||||||
pub async fn async_from(data: &FormData) -> Self {
|
pub async fn async_from(data: &FormData) -> Self {
|
||||||
Self {
|
Self {
|
||||||
value: data.value(),
|
value: data.value(),
|
||||||
values: data.values(),
|
values: data.values(),
|
||||||
valid: data.valid(),
|
valid: data.valid(),
|
||||||
files: match data.files() {
|
#[cfg(feature = "file-engine")]
|
||||||
Some(files) => {
|
files: {
|
||||||
let mut resolved_files = HashMap::new();
|
match data.files() {
|
||||||
|
Some(files) => {
|
||||||
|
let mut resolved_files = HashMap::new();
|
||||||
|
|
||||||
for file in files.files() {
|
for file in files.files() {
|
||||||
let bytes = files.read_file(&file).await;
|
let bytes = files.read_file(&file).await;
|
||||||
resolved_files.insert(file, bytes.unwrap_or_default());
|
resolved_files.insert(file, bytes.unwrap_or_default());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(crate::file_data::SerializedFileEngine {
|
||||||
|
files: resolved_files,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
None => None,
|
||||||
Some(crate::file_data::SerializedFileEngine {
|
|
||||||
files: resolved_files,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
None => None,
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,6 +233,7 @@ impl SerializedFormData {
|
||||||
value: data.value(),
|
value: data.value(),
|
||||||
values: data.values(),
|
values: data.values(),
|
||||||
valid: data.valid(),
|
valid: data.valid(),
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
files: None,
|
files: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,15 +260,17 @@ impl HasFormData for SerializedFormData {
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
impl HasFileData for SerializedFormData {
|
impl HasFileData for SerializedFormData {
|
||||||
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
|
#[cfg(feature = "file-engine")]
|
||||||
|
fn files(&self) -> Option<std::sync::Arc<dyn crate::FileEngine>> {
|
||||||
self.files
|
self.files
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|files| std::sync::Arc::new(files.clone()) as _)
|
.map(|files| std::sync::Arc::new(files.clone()) as _)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
impl HasFileData for FormData {
|
impl HasFileData for FormData {
|
||||||
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
|
fn files(&self) -> Option<std::sync::Arc<dyn crate::FileEngine>> {
|
||||||
self.inner.files()
|
self.inner.files()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
use std::any::Any;
|
|
||||||
|
|
||||||
pub trait HasFileData: std::any::Any {
|
pub trait HasFileData: std::any::Any {
|
||||||
|
// NOTE: The methods of this trait are config'ed out instead of the trait
|
||||||
|
// itself because several other traits inherit from this trait and there isn't a clean way to
|
||||||
|
// conditionally inherit from a trait based on a config.
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
|
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
/// A file engine that serializes files to bytes
|
/// A file engine that serializes files to bytes
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
|
||||||
pub struct SerializedFileEngine {
|
pub struct SerializedFileEngine {
|
||||||
|
@ -14,6 +17,7 @@ pub struct SerializedFileEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl FileEngine for SerializedFileEngine {
|
impl FileEngine for SerializedFileEngine {
|
||||||
fn files(&self) -> Vec<String> {
|
fn files(&self) -> Vec<String> {
|
||||||
|
@ -35,13 +39,14 @@ impl FileEngine for SerializedFileEngine {
|
||||||
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())
|
.map(|bytes| String::from_utf8_lossy(&bytes).to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>> {
|
async fn get_native_file(&self, file: &str) -> Option<Box<dyn std::any::Any>> {
|
||||||
self.read_file(file)
|
self.read_file(file)
|
||||||
.await
|
.await
|
||||||
.map(|val| Box::new(val) as Box<dyn Any>)
|
.map(|val| Box::new(val) as Box<dyn std::any::Any>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
pub trait FileEngine {
|
pub trait FileEngine {
|
||||||
// get a list of file names
|
// get a list of file names
|
||||||
|
@ -57,5 +62,5 @@ pub trait FileEngine {
|
||||||
async fn read_file_to_string(&self, file: &str) -> Option<String>;
|
async fn read_file_to_string(&self, file: &str) -> Option<String>;
|
||||||
|
|
||||||
// returns a file in platform's native representation
|
// returns a file in platform's native representation
|
||||||
async fn get_native_file(&self, file: &str) -> Option<Box<dyn Any>>;
|
async fn get_native_file(&self, file: &str) -> Option<Box<dyn std::any::Any>>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,8 @@ pub mod point_interaction;
|
||||||
mod render_template;
|
mod render_template;
|
||||||
#[cfg(feature = "wasm-bind")]
|
#[cfg(feature = "wasm-bind")]
|
||||||
mod web_sys_bind;
|
mod web_sys_bind;
|
||||||
|
#[cfg(feature = "wasm-bind")]
|
||||||
|
pub use web_sys_bind::*;
|
||||||
|
|
||||||
#[cfg(feature = "serialize")]
|
#[cfg(feature = "serialize")]
|
||||||
mod transit;
|
mod transit;
|
||||||
|
|
|
@ -16,6 +16,7 @@ impl NativeFileEngine {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl FileEngine for NativeFileEngine {
|
impl FileEngine for NativeFileEngine {
|
||||||
fn files(&self) -> Vec<String> {
|
fn files(&self) -> Vec<String> {
|
||||||
|
|
|
@ -35,7 +35,6 @@ fn render_template_node(node: &TemplateNode, out: &mut String) -> std::fmt::Resu
|
||||||
}
|
}
|
||||||
TemplateNode::Text { text: t } => write!(out, "{t}")?,
|
TemplateNode::Text { text: t } => write!(out, "{t}")?,
|
||||||
TemplateNode::Dynamic { id: _ } => write!(out, "<pre hidden />")?,
|
TemplateNode::Dynamic { id: _ } => write!(out, "<pre hidden />")?,
|
||||||
TemplateNode::DynamicText { id: t } => write!(out, "<!-- --> {t} <!-- -->")?,
|
|
||||||
};
|
};
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,15 +3,13 @@ use crate::events::{
|
||||||
AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData,
|
AnimationData, CompositionData, KeyboardData, MouseData, PointerData, TouchData,
|
||||||
TransitionData, WheelData,
|
TransitionData, WheelData,
|
||||||
};
|
};
|
||||||
use crate::file_data::{FileEngine, HasFileData};
|
use crate::file_data::HasFileData;
|
||||||
use crate::geometry::{
|
use crate::geometry::{ClientPoint, ElementPoint, PagePoint, ScreenPoint};
|
||||||
ClientPoint, ElementPoint, PagePoint, PixelsRect, PixelsSize, PixelsVector2D, ScreenPoint,
|
|
||||||
};
|
|
||||||
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
|
use crate::input_data::{decode_key_location, decode_mouse_button_set, MouseButton};
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use keyboard_types::{Code, Key, Modifiers};
|
use keyboard_types::{Code, Key, Modifiers};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use wasm_bindgen::{JsCast, JsValue};
|
use wasm_bindgen::JsCast;
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, Touch,
|
AnimationEvent, CompositionEvent, Event, KeyboardEvent, MouseEvent, PointerEvent, Touch,
|
||||||
TouchEvent, TransitionEvent, WheelEvent,
|
TouchEvent, TransitionEvent, WheelEvent,
|
||||||
|
@ -427,30 +425,41 @@ impl From<&web_sys::Element> for MountedData {
|
||||||
impl crate::RenderedElementBacking for web_sys::Element {
|
impl crate::RenderedElementBacking for web_sys::Element {
|
||||||
fn get_scroll_offset(
|
fn get_scroll_offset(
|
||||||
&self,
|
&self,
|
||||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::MountedResult<PixelsVector2D>>>>
|
) -> std::pin::Pin<
|
||||||
{
|
Box<
|
||||||
|
dyn std::future::Future<Output = crate::MountedResult<crate::geometry::PixelsVector2D>>,
|
||||||
|
>,
|
||||||
|
> {
|
||||||
let left = self.scroll_left();
|
let left = self.scroll_left();
|
||||||
let top = self.scroll_top();
|
let top = self.scroll_top();
|
||||||
let result = Ok(PixelsVector2D::new(left as f64, top as f64));
|
let result = Ok(crate::geometry::PixelsVector2D::new(
|
||||||
|
left as f64,
|
||||||
|
top as f64,
|
||||||
|
));
|
||||||
Box::pin(async { result })
|
Box::pin(async { result })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_scroll_size(
|
fn get_scroll_size(
|
||||||
&self,
|
&self,
|
||||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::MountedResult<PixelsSize>>>>
|
) -> std::pin::Pin<
|
||||||
{
|
Box<dyn std::future::Future<Output = crate::MountedResult<crate::geometry::PixelsSize>>>,
|
||||||
|
> {
|
||||||
let width = self.scroll_width();
|
let width = self.scroll_width();
|
||||||
let height = self.scroll_height();
|
let height = self.scroll_height();
|
||||||
let result = Ok(PixelsSize::new(width as f64, height as f64));
|
let result = Ok(crate::geometry::PixelsSize::new(
|
||||||
|
width as f64,
|
||||||
|
height as f64,
|
||||||
|
));
|
||||||
Box::pin(async { result })
|
Box::pin(async { result })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_client_rect(
|
fn get_client_rect(
|
||||||
&self,
|
&self,
|
||||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::MountedResult<PixelsRect>>>>
|
) -> std::pin::Pin<
|
||||||
{
|
Box<dyn std::future::Future<Output = crate::MountedResult<crate::geometry::PixelsRect>>>,
|
||||||
|
> {
|
||||||
let rect = self.get_bounding_client_rect();
|
let rect = self.get_bounding_client_rect();
|
||||||
let result = Ok(PixelsRect::new(
|
let result = Ok(crate::geometry::PixelsRect::new(
|
||||||
euclid::Point2D::new(rect.left(), rect.top()),
|
euclid::Point2D::new(rect.left(), rect.top()),
|
||||||
euclid::Size2D::new(rect.width(), rect.height()),
|
euclid::Size2D::new(rect.width(), rect.height()),
|
||||||
));
|
));
|
||||||
|
@ -481,6 +490,17 @@ impl crate::RenderedElementBacking for web_sys::Element {
|
||||||
&self,
|
&self,
|
||||||
focus: bool,
|
focus: bool,
|
||||||
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::MountedResult<()>>>> {
|
) -> std::pin::Pin<Box<dyn std::future::Future<Output = crate::MountedResult<()>>>> {
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct FocusError(wasm_bindgen::JsValue);
|
||||||
|
|
||||||
|
impl std::fmt::Display for FocusError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "failed to focus element {:?}", self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::error::Error for FocusError {}
|
||||||
|
|
||||||
let result = self
|
let result = self
|
||||||
.dyn_ref::<web_sys::HtmlElement>()
|
.dyn_ref::<web_sys::HtmlElement>()
|
||||||
.ok_or_else(|| crate::MountedError::OperationFailed(Box::new(FocusError(self.into()))))
|
.ok_or_else(|| crate::MountedError::OperationFailed(Box::new(FocusError(self.into()))))
|
||||||
|
@ -492,17 +512,6 @@ impl crate::RenderedElementBacking for web_sys::Element {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct FocusError(JsValue);
|
|
||||||
|
|
||||||
impl std::fmt::Display for FocusError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "failed to focus element {:?}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for FocusError {}
|
|
||||||
|
|
||||||
impl HasScrollData for Event {
|
impl HasScrollData for Event {
|
||||||
fn as_any(&self) -> &dyn std::any::Any {
|
fn as_any(&self) -> &dyn std::any::Any {
|
||||||
self
|
self
|
||||||
|
@ -546,18 +555,15 @@ impl HasMediaData for web_sys::Event {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HasFileData for web_sys::Event {
|
impl HasFileData for web_sys::Event {
|
||||||
fn files(&self) -> Option<std::sync::Arc<dyn FileEngine>> {
|
#[cfg(feature = "file-engine")]
|
||||||
#[cfg(not(feature = "file_engine"))]
|
fn files(&self) -> Option<std::sync::Arc<dyn crate::file_data::FileEngine>> {
|
||||||
let files = None;
|
let files = self
|
||||||
#[cfg(feature = "file_engine")]
|
|
||||||
let files = element
|
|
||||||
.dyn_ref()
|
.dyn_ref()
|
||||||
.and_then(|input: &web_sys::HtmlInputElement| {
|
.and_then(|input: &web_sys::HtmlInputElement| {
|
||||||
input.files().and_then(|files| {
|
input.files().and_then(|files| {
|
||||||
#[allow(clippy::arc_with_non_send_sync)]
|
#[allow(clippy::arc_with_non_send_sync)]
|
||||||
crate::file_engine::WebFileEngine::new(files).map(|f| {
|
crate::web_sys_bind::file_engine::WebFileEngine::new(files)
|
||||||
std::sync::Arc::new(f) as std::sync::Arc<dyn dioxus_html::FileEngine>
|
.map(|f| std::sync::Arc::new(f) as std::sync::Arc<dyn crate::FileEngine>)
|
||||||
})
|
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
use std::any::Any;
|
use std::any::Any;
|
||||||
|
|
||||||
use dioxus_html::FileEngine;
|
use crate::FileEngine;
|
||||||
use futures_channel::oneshot;
|
use futures_channel::oneshot;
|
||||||
use js_sys::Uint8Array;
|
use js_sys::Uint8Array;
|
||||||
use wasm_bindgen::{prelude::Closure, JsCast};
|
use wasm_bindgen::{prelude::Closure, JsCast};
|
||||||
use web_sys::{File, FileList, FileReader};
|
use web_sys::{File, FileList, FileReader};
|
||||||
|
|
||||||
pub(crate) struct WebFileEngine {
|
/// A file engine for the web platform
|
||||||
|
pub struct WebFileEngine {
|
||||||
file_reader: FileReader,
|
file_reader: FileReader,
|
||||||
file_list: FileList,
|
file_list: FileList,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebFileEngine {
|
impl WebFileEngine {
|
||||||
|
/// Create a new file engine from a file list
|
||||||
pub fn new(file_list: FileList) -> Option<Self> {
|
pub fn new(file_list: FileList) -> Option<Self> {
|
||||||
Some(Self {
|
Some(Self {
|
||||||
file_list,
|
file_list,
|
|
@ -1 +1,5 @@
|
||||||
mod events;
|
mod events;
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
|
mod file_engine;
|
||||||
|
#[cfg(feature = "file-engine")]
|
||||||
|
pub use file_engine::*;
|
||||||
|
|
|
@ -1,21 +1,29 @@
|
||||||
use std::collections::hash_map::DefaultHasher;
|
use std::collections::hash_map::DefaultHasher;
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::{hash::Hasher, process::Command};
|
use std::{hash::Hasher, process::Command};
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
// If any TS changes, re-run the build script
|
// If any TS changes, re-run the build script
|
||||||
println!("cargo:rerun-if-changed=src/ts/form.ts");
|
let watching = std::fs::read_dir("./src/ts").unwrap();
|
||||||
println!("cargo:rerun-if-changed=src/ts/core.ts");
|
let ts_paths: Vec<_> = watching
|
||||||
println!("cargo:rerun-if-changed=src/ts/serialize.ts");
|
.into_iter()
|
||||||
println!("cargo:rerun-if-changed=src/ts/set_attribute.ts");
|
.flatten()
|
||||||
println!("cargo:rerun-if-changed=src/ts/common.ts");
|
.map(|entry| entry.path())
|
||||||
println!("cargo:rerun-if-changed=src/ts/eval.ts");
|
.filter(|path| path.extension().map(|ext| ext == "ts").unwrap_or(false))
|
||||||
println!("cargo:rerun-if-changed=src/ts/native_eval.ts");
|
.collect();
|
||||||
|
for path in &ts_paths {
|
||||||
|
println!("cargo:rerun-if-changed={}", path.display());
|
||||||
|
}
|
||||||
|
|
||||||
// Compute the hash of the ts files
|
// Compute the hash of the ts files
|
||||||
let hash = hash_ts_files();
|
let hash = hash_ts_files(ts_paths);
|
||||||
|
|
||||||
// If the hash matches the one on disk, we're good and don't need to update bindings
|
// If the hash matches the one on disk, we're good and don't need to update bindings
|
||||||
let expected = include_str!("src/js/hash.txt").trim();
|
let fs_hash_string = std::fs::read_to_string("src/js/hash.txt");
|
||||||
|
let expected = fs_hash_string
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| s.trim())
|
||||||
|
.unwrap_or_default();
|
||||||
if expected == hash.to_string() {
|
if expected == hash.to_string() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -27,24 +35,21 @@ fn main() {
|
||||||
gen_bindings("core", "core");
|
gen_bindings("core", "core");
|
||||||
gen_bindings("eval", "eval");
|
gen_bindings("eval", "eval");
|
||||||
gen_bindings("native_eval", "native_eval");
|
gen_bindings("native_eval", "native_eval");
|
||||||
|
gen_bindings("hydrate", "hydrate");
|
||||||
|
gen_bindings("initialize_streaming", "initialize_streaming");
|
||||||
|
|
||||||
std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
|
std::fs::write("src/js/hash.txt", hash.to_string()).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Hashes the contents of a directory
|
/// Hashes the contents of a directory
|
||||||
fn hash_ts_files() -> u64 {
|
fn hash_ts_files(mut files: Vec<PathBuf>) -> u64 {
|
||||||
let files = [
|
// Different systems will read the files in different orders, so we sort them to make sure the hash is consistent
|
||||||
include_str!("src/ts/common.ts"),
|
files.sort();
|
||||||
include_str!("src/ts/native.ts"),
|
|
||||||
include_str!("src/ts/core.ts"),
|
|
||||||
include_str!("src/ts/eval.ts"),
|
|
||||||
include_str!("src/ts/native_eval.ts"),
|
|
||||||
];
|
|
||||||
|
|
||||||
let mut hash = DefaultHasher::new();
|
let mut hash = DefaultHasher::new();
|
||||||
for file in files {
|
for file in files {
|
||||||
|
let contents = std::fs::read_to_string(file).unwrap();
|
||||||
// windows + git does a weird thing with line endings, so we need to normalize them
|
// windows + git does a weird thing with line endings, so we need to normalize them
|
||||||
for line in file.lines() {
|
for line in contents.lines() {
|
||||||
hash.write(line.as_bytes());
|
hash.write(line.as_bytes());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){if(this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},handler)this.handler=handler}createListener(event_name,element,bubbles){if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}loadChild(ptr,len){let node=this.stack[this.stack.length-1],ptr_end=ptr+len;for(;ptr<ptr_end;ptr++){let end=this.m.getUint8(ptr);for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrate(ids){const hydrateNodes=document.querySelectorAll("[data-node-hydration]");for(let i=0;i<hydrateNodes.length;i++){const hydrateNode=hydrateNodes[i],split=hydrateNode.getAttribute("data-node-hydration").split(","),id=ids[parseInt(split[0])];if(this.nodes[id]=hydrateNode,split.length>1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j<split.length;j++){const split2=split[j].split(":"),event_name=split2[0],bubbles=split2[1]==="1";this.createListener(event_name,hydrateNode,bubbles)}}}const treeWalker=document.createTreeWalker(document.body,NodeFilter.SHOW_COMMENT);let currentNode=treeWalker.nextNode();while(currentNode){const split=currentNode.textContent.split("node-id");if(split.length>1){let next=currentNode.nextSibling;if(next.nodeType===Node.COMMENT_NODE)next=next.parentElement.insertBefore(document.createTextNode(""),next);this.nodes[ids[parseInt(split[1])]]=next}currentNode=treeWalker.nextNode()}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter};
|
function setAttributeInner(node,field,value,ns){if(ns==="style"){node.style.setProperty(field,value);return}if(ns){node.setAttributeNS(ns,field,value);return}switch(field){case"value":if(node.value!==value)node.value=value;break;case"initial_value":node.defaultValue=value;break;case"checked":node.checked=truthy(value);break;case"initial_checked":node.defaultChecked=truthy(value);break;case"selected":node.selected=truthy(value);break;case"initial_selected":node.defaultSelected=truthy(value);break;case"dangerous_inner_html":node.innerHTML=value;break;default:if(!truthy(value)&&isBoolAttr(field))node.removeAttribute(field);else node.setAttribute(field,value)}}var truthy=function(val){return val==="true"||val===!0},isBoolAttr=function(field){switch(field){case"allowfullscreen":case"allowpaymentrequest":case"async":case"autofocus":case"autoplay":case"checked":case"controls":case"default":case"defer":case"disabled":case"formnovalidate":case"hidden":case"ismap":case"itemscope":case"loop":case"multiple":case"muted":case"nomodule":case"novalidate":case"open":case"playsinline":case"readonly":case"required":case"reversed":case"selected":case"truespeed":case"webkitdirectory":return!0;default:return!1}};class BaseInterpreter{global;local;root;handler;nodes;stack;templates;m;constructor(){}initialize(root,handler=null){if(this.global={},this.local={},this.root=root,this.nodes=[root],this.stack=[root],this.templates={},handler)this.handler=handler}createListener(event_name,element,bubbles){if(bubbles)if(this.global[event_name]===void 0)this.global[event_name]={active:1,callback:this.handler},this.root.addEventListener(event_name,this.handler);else this.global[event_name].active++;else{const id=element.getAttribute("data-dioxus-id");if(!this.local[id])this.local[id]={};element.addEventListener(event_name,this.handler)}}removeListener(element,event_name,bubbles){if(bubbles)this.removeBubblingListener(event_name);else this.removeNonBubblingListener(element,event_name)}removeBubblingListener(event_name){if(this.global[event_name].active--,this.global[event_name].active===0)this.root.removeEventListener(event_name,this.global[event_name].callback),delete this.global[event_name]}removeNonBubblingListener(element,event_name){const id=element.getAttribute("data-dioxus-id");if(delete this.local[id][event_name],Object.keys(this.local[id]).length===0)delete this.local[id];element.removeEventListener(event_name,this.handler)}removeAllNonBubblingListeners(element){const id=element.getAttribute("data-dioxus-id");delete this.local[id]}getNode(id){return this.nodes[id]}pushRoot(node){this.stack.push(node)}appendChildren(id,many){const root=this.nodes[id],els=this.stack.splice(this.stack.length-many);for(let k=0;k<many;k++)root.appendChild(els[k])}loadChild(ptr,len){let node=this.stack[this.stack.length-1],ptr_end=ptr+len;for(;ptr<ptr_end;ptr++){let end=this.m.getUint8(ptr);for(node=node.firstChild;end>0;end--)node=node.nextSibling}return node}saveTemplate(nodes,tmpl_id){this.templates[tmpl_id]=nodes}hydrate_node(hydrateNode,ids){const split=hydrateNode.getAttribute("data-node-hydration").split(","),id=ids[parseInt(split[0])];if(this.nodes[id]=hydrateNode,split.length>1){hydrateNode.listening=split.length-1,hydrateNode.setAttribute("data-dioxus-id",id.toString());for(let j=1;j<split.length;j++){const split2=split[j].split(":"),event_name=split2[0],bubbles=split2[1]==="1";this.createListener(event_name,hydrateNode,bubbles)}}}hydrate(ids,underNodes){for(let i=0;i<underNodes.length;i++){const under=underNodes[i];if(under instanceof HTMLElement){if(under.getAttribute("data-node-hydration"))this.hydrate_node(under,ids);const hydrateNodes=under.querySelectorAll("[data-node-hydration]");for(let i2=0;i2<hydrateNodes.length;i2++)this.hydrate_node(hydrateNodes[i2],ids)}const treeWalker=document.createTreeWalker(under,NodeFilter.SHOW_COMMENT);while(treeWalker.currentNode){const currentNode=treeWalker.currentNode;if(currentNode.nodeType===Node.COMMENT_NODE){const split=currentNode.textContent.split("node-id");if(split.length>1){let next=currentNode.nextSibling;currentNode.remove();let commentAfterText,textNode;if(next.nodeType===Node.COMMENT_NODE){const newText=next.parentElement.insertBefore(document.createTextNode(""),next);commentAfterText=next,textNode=newText}else textNode=next,commentAfterText=textNode.nextSibling;treeWalker.currentNode=commentAfterText,this.nodes[ids[parseInt(split[1])]]=textNode;let exit=!treeWalker.nextNode();if(commentAfterText.remove(),exit)break}else if(!treeWalker.nextNode())break}else if(!treeWalker.nextNode())break}}}setAttributeInner(node,field,value,ns){setAttributeInner(node,field,value,ns)}}export{BaseInterpreter};
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue