diff --git a/Cargo.lock b/Cargo.lock index cbec509f0c..4ccc9156b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,7 +64,7 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "once_cell", "version_check", ] @@ -77,7 +77,7 @@ checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "const-random", - "getrandom 0.2.14", + "getrandom 0.2.15", "once_cell", "version_check", "zerocopy", @@ -320,6 +320,17 @@ dependencies = [ "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]] name = "async-channel" version = "2.2.1" @@ -357,8 +368,8 @@ checksum = "b10202063978b3351199d68f8b22c4e47e4b1b822f8d43fd862d5ea8c006b29a" dependencies = [ "async-task", "concurrent-queue", - "fastrand", - "futures-lite", + "fastrand 2.0.2", + "futures-lite 2.3.0", "slab", ] @@ -368,9 +379,44 @@ version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc19683171f287921f2405677dd2ed2549c3b3bda697a563ebc3a121ace2aba1" dependencies = [ - "async-lock", + "async-lock 3.3.0", + "blocking", + "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", + "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]] @@ -379,19 +425,28 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcccb0f599cfa2f8ace422d3555572f47424da5648a4382a9dd0310ff8210884" dependencies = [ - "async-lock", + "async-lock 3.3.0", "cfg-if", "concurrent-queue", "futures-io", - "futures-lite", + "futures-lite 2.3.0", "parking", - "polling", - "rustix", + "polling 3.7.0", + "rustix 0.38.34", "slab", "tracing", "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]] name = "async-lock" version = "3.3.0" @@ -409,9 +464,9 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ - "async-io", + "async-io 2.3.2", "blocking", - "futures-lite", + "futures-lite 2.3.0", ] [[package]] @@ -420,16 +475,16 @@ version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a53fc6301894e04a92cb2584fedde80cb25ba8e02d9dc39d4a87d036e22f397d" dependencies = [ - "async-channel", - "async-io", - "async-lock", + "async-channel 2.2.1", + "async-io 2.3.2", + "async-lock 3.3.0", "async-signal", "async-task", "blocking", "cfg-if", "event-listener 5.3.0", - "futures-lite", - "rustix", + "futures-lite 2.3.0", + "rustix 0.38.34", "tracing", "windows-sys 0.52.0", ] @@ -451,18 +506,44 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afe66191c335039c7bb78f99dc7520b0cbb166b3a1cb33a03f53d8a1c6f2afda" dependencies = [ - "async-io", - "async-lock", + "async-io 2.3.2", + "async-lock 3.3.0", "atomic-waker", "cfg-if", "futures-core", "futures-io", - "rustix", + "rustix 0.38.34", "signal-hook-registry", "slab", "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]] name = "async-task" version = "4.7.0" @@ -689,7 +770,7 @@ name = "axum-hello-world" version = "0.1.0" dependencies = [ "dioxus", - "reqwest", + "reqwest 0.11.27", "serde", "simple_logger", "tracing", @@ -942,12 +1023,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a37913e8dc4ddcc604f0c6d3bf2887c995153af3611de9e23c352b44c1b9118" dependencies = [ - "async-channel", - "async-lock", + "async-channel 2.2.1", + "async-lock 3.3.0", "async-task", - "fastrand", + "fastrand 2.0.2", "futures-io", - "futures-lite", + "futures-lite 2.3.0", "piper", "tracing", ] @@ -1535,7 +1616,7 @@ version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "once_cell", "tiny-keccak", ] @@ -2223,7 +2304,7 @@ dependencies = [ "openssl", "prettyplease", "rayon", - "reqwest", + "reqwest 0.11.27", "rsx-rosetta", "serde", "serde_json", @@ -2291,6 +2372,7 @@ name = "dioxus-core" version = "0.5.2" dependencies = [ "dioxus", + "dioxus-html", "dioxus-ssr", "futures-channel", "futures-util", @@ -2298,7 +2380,7 @@ dependencies = [ "longest-increasing-subsequence", "pretty_assertions", "rand 0.8.5", - "reqwest", + "reqwest 0.11.27", "rustc-hash", "rustversion", "serde", @@ -2327,12 +2409,6 @@ dependencies = [ "trybuild", ] -[[package]] -name = "dioxus-debug-cell" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ea539174bb236e0e7dc9c12b19b88eae3cb574dedbd0252a2d43ea7e6de13e2" - [[package]] name = "dioxus-desktop" version = "0.5.2" @@ -2360,7 +2436,7 @@ dependencies = [ "objc", "objc_id", "rand 0.8.5", - "reqwest", + "reqwest 0.11.27", "rfd", "rustc-hash", "separator", @@ -2387,11 +2463,11 @@ dependencies = [ "dioxus-ssr", "form_urlencoded", "futures-util", - "getrandom 0.2.14", + "getrandom 0.2.15", "http-range", "manganis", "rand 0.8.5", - "reqwest", + "reqwest 0.11.27", "separator", "serde", "serde_json", @@ -2422,18 +2498,20 @@ dependencies = [ "dioxus-cli-config", "dioxus-desktop", "dioxus-hot-reload", + "dioxus-interpreter-js", "dioxus-lib", "dioxus-mobile", "dioxus-ssr", "dioxus-web", "dioxus_server_macro", + "futures-channel", "futures-util", "http 1.1.0", "hyper 1.3.1", "once_cell", + "parking_lot", "pin-project", "serde", - "serde_json", "server_fn", "thiserror", "tokio", @@ -2447,21 +2525,32 @@ dependencies = [ "web-sys", ] +[[package]] +name = "dioxus-hackernews" +version = "0.1.0" +dependencies = [ + "chrono", + "dioxus", + "reqwest 0.12.4", + "serde", + "tracing", + "tracing-subscriber", + "tracing-wasm", +] + [[package]] name = "dioxus-hooks" version = "0.5.2" dependencies = [ "dioxus", "dioxus-core 0.5.2", - "dioxus-debug-cell", "dioxus-signals", "futures-channel", "futures-util", "generational-box 0.5.2", - "reqwest", + "reqwest 0.11.27", "rustversion", "slab", - "thiserror", "tokio", "tracing", "web-sys", @@ -2503,6 +2592,7 @@ dependencies = [ "euclid", "futures-channel", "generational-box 0.5.2", + "js-sys", "keyboard-types", "rfd", "rustversion", @@ -2700,7 +2790,7 @@ dependencies = [ "once_cell", "parking_lot", "rand 0.8.5", - "reqwest", + "reqwest 0.11.27", "rustc-hash", "serde", "simple_logger", @@ -2722,6 +2812,7 @@ dependencies = [ "dioxus-cli-config", "dioxus-core 0.5.2", "dioxus-html", + "dioxus-interpreter-js", "dioxus-signals", "fern", "fs_extra", @@ -2768,7 +2859,7 @@ dependencies = [ name = "dioxus-web" version = "0.5.2" dependencies = [ - "async-trait", + "ciborium", "console_error_panic_hook", "dioxus", "dioxus-core 0.5.2", @@ -3191,6 +3282,15 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183" +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + [[package]] name = "fastrand" version = "2.0.2" @@ -3451,13 +3551,28 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "futures-lite" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ - "fastrand", + "fastrand 2.0.2", "futures-core", "futures-io", "parking", @@ -3663,9 +3778,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "js-sys", @@ -3976,7 +4091,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc" dependencies = [ - "fastrand", + "fastrand 2.0.2", "unicode-normalization", ] @@ -4226,6 +4341,8 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" dependencies = [ + "futures-channel", + "futures-core", "js-sys", "wasm-bindgen", ] @@ -4740,7 +4857,7 @@ dependencies = [ "httpdate", "itoa 1.0.11", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower-service", "tracing", @@ -4814,6 +4931,22 @@ dependencies = [ "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]] name = "hyper-util" version = "0.1.3" @@ -4827,7 +4960,7 @@ dependencies = [ "http-body 1.0.0", "hyper 1.3.1", "pin-project-lite", - "socket2", + "socket2 0.5.6", "tokio", "tower", "tower-service", @@ -5137,16 +5270,27 @@ version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" 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]] name = "ipconfig" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" dependencies = [ - "socket2", + "socket2 0.5.6", "widestring", "windows-sys 0.48.0", - "winreg", + "winreg 0.50.0", ] [[package]] @@ -5391,6 +5535,15 @@ dependencies = [ "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]] name = "lazy_static" version = "1.4.0" @@ -5414,9 +5567,9 @@ checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libflate" @@ -5548,7 +5701,7 @@ dependencies = [ "cssparser-color", "dashmap", "data-encoding", - "getrandom 0.2.14", + "getrandom 0.2.15", "itertools 0.10.5", "lazy_static", "parcel_selectors", @@ -5587,6 +5740,12 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -5776,7 +5935,7 @@ dependencies = [ "railwind", "ravif", "rayon", - "reqwest", + "reqwest 0.11.27", "rustc-hash", "serde", "toml 0.7.8", @@ -5794,7 +5953,7 @@ dependencies = [ "base64 0.21.7", "home", "infer 0.11.0", - "reqwest", + "reqwest 0.11.27", "serde", "toml 0.7.8", "tracing", @@ -6109,6 +6268,15 @@ dependencies = [ "jni-sys", ] +[[package]] +name = "nested-suspense" +version = "0.1.0" +dependencies = [ + "dioxus", + "serde", + "tokio", +] + [[package]] name = "new_debug_unreachable" version = "1.0.6" @@ -6905,7 +7073,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "668d31b1c4eba19242f2088b2bf3316b82ca31082a8335764db4e083db7485d4" dependencies = [ "atomic-waker", - "fastrand", + "fastrand 2.0.2", "futures-io", ] @@ -6991,6 +7159,22 @@ dependencies = [ "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]] name = "polling" version = "3.7.0" @@ -7001,7 +7185,7 @@ dependencies = [ "concurrent-queue", "hermit-abi 0.3.9", "pin-project-lite", - "rustix", + "rustix 0.38.34", "tracing", "windows-sys 0.52.0", ] @@ -7311,7 +7495,7 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", ] [[package]] @@ -7438,7 +7622,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "libredox", "thiserror", ] @@ -7529,7 +7713,7 @@ dependencies = [ "http-body 0.4.6", "hyper 0.14.28", "hyper-rustls 0.24.2", - "hyper-tls", + "hyper-tls 0.5.0", "ipnet", "js-sys", "log", @@ -7557,7 +7741,49 @@ dependencies = [ "wasm-streams", "web-sys", "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]] @@ -7638,7 +7864,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.14", + "getrandom 0.2.15", "libc", "spin 0.9.8", "untrusted", @@ -7773,6 +7999,20 @@ dependencies = [ "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]] name = "rustix" version = "0.38.34" @@ -7782,7 +8022,7 @@ dependencies = [ "bitflags 2.5.0", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.13", "windows-sys 0.52.0", ] @@ -8185,7 +8425,7 @@ dependencies = [ "inventory", "js-sys", "once_cell", - "reqwest", + "reqwest 0.11.27", "send_wrapper", "serde", "serde_json", @@ -8437,6 +8677,16 @@ dependencies = [ "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]] name = "socket2" version = "0.5.6" @@ -8846,6 +9096,15 @@ dependencies = [ "is_ci", ] +[[package]] +name = "suspense-carousel" +version = "0.5.2" +dependencies = [ + "async-std", + "dioxus", + "serde", +] + [[package]] name = "syn" version = "1.0.109" @@ -9036,7 +9295,7 @@ dependencies = [ "uuid", "walkdir", "windows-sys 0.48.0", - "winreg", + "winreg 0.50.0", "zip", ] @@ -9084,8 +9343,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", - "fastrand", - "rustix", + "fastrand 2.0.2", + "rustix 0.38.34", "windows-sys 0.52.0", ] @@ -9268,7 +9527,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.6", "tokio-macros", "windows-sys 0.48.0", ] @@ -9792,7 +10051,7 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ - "getrandom 0.2.14", + "getrandom 0.2.15", "serde", "sha1_smol", ] @@ -9844,6 +10103,12 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65dd7eed29412da847b0f78bcec0ac98588165988a8cfe41d4ea1d429f8ccfff" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "walkdir" version = "2.5.0" @@ -10291,7 +10556,7 @@ dependencies = [ "either", "home", "once_cell", - "rustix", + "rustix 0.38.34", ] [[package]] @@ -10679,6 +10944,16 @@ dependencies = [ "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]] name = "wry" version = "0.37.0" @@ -10761,8 +11036,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8da84f1a25939b27f6820d92aed108f83ff920fdf11a7b19366c27c4cda81d4f" dependencies = [ "libc", - "linux-raw-sys", - "rustix", + "linux-raw-sys 0.4.13", + "rustix 0.38.34", ] [[package]] @@ -10796,8 +11071,8 @@ dependencies = [ "async-broadcast", "async-executor", "async-fs", - "async-io", - "async-lock", + "async-io 2.3.2", + "async-lock 3.3.0", "async-process", "async-recursion", "async-task", diff --git a/Cargo.toml b/Cargo.toml index bc52a1ad87..00bdb60dab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,6 +35,7 @@ members = [ "packages/fullstack/examples/axum-streaming", "packages/fullstack/examples/axum-desktop", "packages/fullstack/examples/axum-auth", + "packages/fullstack/examples/hackernews", "packages/static-generation/examples/simple", "packages/static-generation/examples/router", "packages/static-generation/examples/github-pages", @@ -46,6 +47,8 @@ members = [ "packages/playwright-tests/liveview", "packages/playwright-tests/web", "packages/playwright-tests/fullstack", + "packages/playwright-tests/suspense-carousel", + "packages/playwright-tests/nested-suspense", ] 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-router = { path = "packages/router", 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-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-desktop = { path = "packages/desktop", version = "0.5.0", default-features = false } dioxus-mobile = { path = "packages/mobile", version = "0.5.0" } @@ -118,6 +121,9 @@ axum_session_auth = "0.12.1" axum-extra = "0.9.2" reqwest = "0.11.24" 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 [profile.dev.package.insta] @@ -275,7 +281,7 @@ required-features = ["desktop"] doc-scrape-examples = true [[example]] -name = "error_handle" +name = "errors" required-features = ["desktop"] doc-scrape-examples = true diff --git a/examples/README.md b/examples/README.md index 37fb14da33..2d65585121 100644 --- a/examples/README.md +++ b/examples/README.md @@ -74,7 +74,7 @@ cargo run --example hello_world [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 diff --git a/examples/error_handle.rs b/examples/error_handle.rs deleted file mode 100644 index 5c3279940e..0000000000 --- a/examples/error_handle.rs +++ /dev/null @@ -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 = 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") -} diff --git a/examples/errors.rs b/examples/errors.rs new file mode 100644 index 0000000000..12b7fbaa8e --- /dev/null +++ b/examples/errors.rs @@ -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:: {} }); +} + +/// 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" + } + } + } + } +} diff --git a/examples/rsx_usage.rs b/examples/rsx_usage.rs index 9f3dd9750e..6fb2c8c739 100644 --- a/examples/rsx_usage.rs +++ b/examples/rsx_usage.rs @@ -170,13 +170,13 @@ fn app() -> Element { // 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 }) } // Spreading can also be overridden manually Taller { - ..TallerProps { a: "ballin!", children: None }, + ..TallerProps { a: "ballin!", children: VNode::empty() }, a: "not ballin!" } @@ -193,8 +193,8 @@ fn app() -> Element { // Type inference can be used too TypedInput { initial: 10.0 } - // geneircs with the `inline_props` macro - Label { text: "hello geneirc world!" } + // generic with the `inline_props` macro + Label { text: "hello generic world!" } Label { text: 99.9 } // Lowercase components work too, as long as they are access using a path @@ -283,7 +283,7 @@ where return rsx! { "{props}" }; } - None + VNode::empty() } #[component] diff --git a/examples/shorthand.rs b/examples/shorthand.rs index 995dac5e19..33905d5710 100644 --- a/examples/shorthand.rs +++ b/examples/shorthand.rs @@ -22,7 +22,7 @@ fn app() -> Element { rsx! { div { class, id, {&children} } 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() } } } } diff --git a/examples/suspense.rs b/examples/suspense.rs index df3151f22b..014bb3276a 100644 --- a/examples/suspense.rs +++ b/examples/suspense.rs @@ -39,7 +39,14 @@ fn app() -> Element { } 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. #[component] fn Doggo() -> Element { - let mut fut = use_resource(move || async move { + let mut resource = use_resource(move || async move { #[derive(serde::Deserialize)] struct DogApi { message: String, @@ -62,12 +69,26 @@ fn Doggo() -> Element { .await }); - match fut.read_unchecked().as_ref() { - Some(Ok(resp)) => rsx! { - button { onclick: move |_| fut.restart(), "Click to fetch another doggo" } + // You can suspend the future and only continue rendering when it's ready + let value = resource.suspend().with_loading_placeholder(|| { + 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}" } } }, - Some(Err(_)) => rsx! { div { "loading dogs failed" } }, - None => rsx! { div { "loading dogs..." } }, + Err(_) => rsx! { + div { "loading dogs failed" } + button { + onclick: move |_| resource.restart(), + "retry" + } + }, } } diff --git a/packages/cli-config/src/lib.rs b/packages/cli-config/src/lib.rs index db10f70e22..fee949f848 100644 --- a/packages/cli-config/src/lib.rs +++ b/packages/cli-config/src/lib.rs @@ -17,10 +17,21 @@ pub use serve::*; pub mod __private { 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 { 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 } @@ -30,6 +41,7 @@ pub mod __private { impl Drop for CrateConfigDropGuard { fn drop(&mut self) { 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")] /// The current crate's configuration. 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"); diff --git a/packages/cli/src/assets.rs b/packages/cli/src/assets.rs index 87e66236b2..6ed61cb15e 100644 --- a/packages/cli/src/assets.rs +++ b/packages/cli/src/assets.rs @@ -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 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!( "Failed to pre-compress static assets {}: {}", - entry_path.display(), + output_file_location.display(), 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(()) } -/// pre-compress a file with brotli -pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> { +/// Get the path to the compressed version of a file +fn compressed_path(path: &Path) -> Option { let new_extension = match path.extension() { Some(ext) => { if ext.to_string_lossy().to_lowercase().ends_with("br") { - return Ok(()); + return None; } let mut ext = ext.to_os_string(); ext.push(".br"); @@ -137,22 +140,37 @@ pub(crate) fn pre_compress_file(path: &Path) -> std::io::Result<()> { } 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 mut stream = std::io::BufReader::new(file); - let output = path.with_extension(new_extension); - let mut buffer = std::fs::File::create(output)?; + let mut buffer = std::fs::File::create(compressed_path)?; let params = BrotliEncoderParams::default(); brotli::BrotliCompress(&mut stream, &mut buffer, ¶ms)?; Ok(()) } /// 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); for entry in walk_dir.into_iter().filter_map(|e| e.ok()) { let entry_path = entry.path(); 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(()) diff --git a/packages/cli/src/assets/index.html b/packages/cli/src/assets/index.html index ed3d481350..b39b673dca 100644 --- a/packages/cli/src/assets/index.html +++ b/packages/cli/src/assets/index.html @@ -1,22 +1,38 @@ - - {app_title} - - - - {style_include} - - -
- - {script_include} - - \ No newline at end of file + + {app_title} + + + + + + {style_include} + + +
+ + {script_include} + + diff --git a/packages/cli/src/builder.rs b/packages/cli/src/builder.rs index 2d36429ab7..abf05a53b0 100644 --- a/packages/cli/src/builder.rs +++ b/packages/cli/src/builder.rs @@ -230,9 +230,7 @@ pub fn build_web( } // 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)?; - } + pre_compress_folder(&bindgen_outdir, config.should_pre_compress_web_assets())?; // [5][OPTIONAL] If tailwind is enabled and installed we run it to generate the CSS let dioxus_tools = dioxus_config.application.tools.clone(); @@ -624,6 +622,16 @@ pub fn gen_page(config: &CrateConfig, manifest: Option<&AssetManifest>, serve: b + + for ConfigOptsBuild { server_feature: serve.server_feature, skip_assets: serve.skip_assets, force_debug: serve.force_debug, + force_sequential: serve.force_sequential, cargo_args: serve.cargo_args, } } @@ -104,6 +110,11 @@ pub struct ConfigOptsServe { #[serde(default)] 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] #[clap(long)] #[serde(default)] diff --git a/packages/cli/src/server/desktop/mod.rs b/packages/cli/src/server/desktop/mod.rs index 8527e2bb96..0d6e6682e1 100644 --- a/packages/cli/src/server/desktop/mod.rs +++ b/packages/cli/src/server/desktop/mod.rs @@ -240,17 +240,11 @@ fn send_msg(msg: HotReloadMsg, channel: &mut impl std::io::Write) -> bool { } } -fn start_desktop( - config: &CrateConfig, - skip_assets: bool, - rust_flags: Option, +fn run_desktop( args: &Vec, env: Vec<(String, String)>, + result: 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 child = RAIIChild( Command::new( @@ -278,13 +272,12 @@ impl DesktopPlatform { /// `rust_flags` argument is added because it is used by the /// `DesktopPlatform`'s implementation of the `Platform::start()`. pub fn start_with_options( + build_result: BuildResult, config: &CrateConfig, serve: &ConfigOptsServe, - rust_flags: Option, env: Vec<(String, String)>, ) -> Result { - let (child, first_build_result) = - start_desktop(config, serve.skip_assets, rust_flags, &serve.args, env)?; + let (child, first_build_result) = run_desktop(&serve.args, env, build_result)?; 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 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; Ok(result) } @@ -349,11 +344,8 @@ impl Platform for DesktopPlatform { serve: &ConfigOptsServe, env: Vec<(String, String)>, ) -> Result { - // See `start_with_options()`'s docs for the explanation why the code - // was moved there. - // Since desktop platform doesn't use `rust_flags`, this argument is - // explicitly set to `None`. - DesktopPlatform::start_with_options(config, serve, None, env) + let build_result = crate::builder::build_desktop(config, true, serve.skip_assets, None)?; + DesktopPlatform::start_with_options(build_result, config, serve, env) } fn rebuild( diff --git a/packages/cli/src/server/fullstack/mod.rs b/packages/cli/src/server/fullstack/mod.rs index 428670739f..6fedcda817 100644 --- a/packages/cli/src/server/fullstack/mod.rs +++ b/packages/cli/src/server/fullstack/mod.rs @@ -5,7 +5,10 @@ use crate::{ BuildResult, Result, }; -use super::{desktop, Platform}; +use super::{ + desktop::{self, DesktopPlatform}, + Platform, +}; static CLIENT_RUST_FLAGS: &str = "-C debuginfo=none -C strip=debuginfo"; // 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 { 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 features = &mut desktop_config.features; match features { @@ -89,16 +94,20 @@ impl Platform for FullstackPlatform { let server_rust_flags = server_rust_flags(&serve.clone().into()); let mut desktop_env = env.clone(); 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, - serve, + true, + serve.skip_assets, Some(server_rust_flags.clone()), - desktop_env, )?; thread_handle .join() .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 { crate::server::web::open_browser( config, @@ -157,7 +166,7 @@ fn build_web(serve: ConfigOptsServe, target_directory: &std::path::Path) -> Resu } .build( None, - Some(target_directory), + (!web_config.force_sequential).then_some(target_directory), Some(client_rust_flags(&web_config)), ) } diff --git a/packages/core-macro/src/props/mod.rs b/packages/core-macro/src/props/mod.rs index 186389e2d4..fda59c7ed8 100644 --- a/packages/core-macro/src/props/mod.rs +++ b/packages/core-macro/src/props/mod.rs @@ -201,9 +201,8 @@ mod field_info { // children field is automatically defaulted to None if name == "children" { - builder_attr.default = Some( - syn::parse(quote!(::core::default::Default::default()).into()).unwrap(), - ); + builder_attr.default = + Some(syn::parse(quote!(dioxus_core::VNode::empty()).into()).unwrap()); } // String fields automatically use impl Display @@ -1046,7 +1045,6 @@ Finally, call `.build()` to create the instance of `{name}`. ty: field_type, .. } = field; - // Add the bump lifetime to the generics let mut ty_generics: Vec = self .generics .params @@ -1198,7 +1196,6 @@ Finally, call `.build()` to create the instance of `{name}`. name: ref field_name, .. } = field; - // Add a bump lifetime to the generics let mut builder_generics: Vec = self .generics .params diff --git a/packages/core-macro/tests/values_memoize_in_place.rs b/packages/core-macro/tests/values_memoize_in_place.rs index 3f7d6f314b..b40134eb88 100644 --- a/packages/core-macro/tests/values_memoize_in_place.rs +++ b/packages/core-macro/tests/values_memoize_in_place.rs @@ -23,7 +23,7 @@ async fn values_memoize_in_place() { use_hook(|| { spawn(async move { 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; } }); @@ -36,8 +36,9 @@ async fn values_memoize_in_place() { let _ = &x; 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(); println!("{:#?}", mutations); - dom.mark_dirty(ScopeId::ROOT); - for _ in 0..20 { + dom.mark_dirty(ScopeId::APP); + for _ in 0..40 { dom.handle_event( "click", Rc::new(PlatformEventData::new(Box::::default())), @@ -74,7 +75,8 @@ fn cloning_event_handler_components_work() { TakesEventHandler { click: move |evt| { println!("Clicked {evt:?}!"); - } + }, + number: 0 } }; @@ -91,7 +93,7 @@ fn cloning_event_handler_components_work() { let mutations = dom.rebuild_to_vec(); println!("{:#?}", mutations); - dom.mark_dirty(ScopeId::ROOT); + dom.mark_dirty(ScopeId::APP); for _ in 0..20 { dom.handle_event( "click", @@ -105,24 +107,23 @@ fn cloning_event_handler_components_work() { } #[component] -fn TakesEventHandler(click: EventHandler, children: usize) -> Element { - println!("children is{children}"); +fn TakesEventHandler(click: EventHandler, number: usize) -> Element { let first_render_click = use_hook(move || click); if generation() > 0 { // Make sure the event handler is memoized in place and never gets dropped - first_render_click(children); + first_render_click(number); } rsx! { button { - onclick: move |_| click(children), - "{children}" + onclick: move |_| click(number), + "{number}" } } } #[component] -fn TakesSignal(sig: ReadOnlySignal, children: usize) -> Element { +fn TakesSignal(sig: ReadOnlySignal, number: usize) -> Element { let first_render_sig = use_hook(move || sig); if generation() > 0 { // Make sure the signal is memoized in place and never gets dropped @@ -130,6 +131,6 @@ fn TakesSignal(sig: ReadOnlySignal, children: usize) -> Element { } rsx! { - button { "{children}" } + button { "{number}" } } } diff --git a/packages/core/Cargo.toml b/packages/core/Cargo.toml index 7f5e5a6221..a3e60f89f3 100644 --- a/packages/core/Cargo.toml +++ b/packages/core/Cargo.toml @@ -21,7 +21,6 @@ slotmap = { workspace = true } futures-channel = { workspace = true } tracing = { workspace = true } serde = { version = "1", features = ["derive"], optional = true } -tracing-subscriber = "0.3.18" generational-box = { workspace = true } rustversion = "1.0.17" @@ -29,20 +28,22 @@ rustversion = "1.0.17" tokio = { workspace = true, features = ["full"] } tracing-fluent-assertions = "0.3.0" dioxus = { workspace = true } +dioxus-html = { workspace = true, features = ["serialize"] } pretty_assertions = "1.3.0" rand = "0.8.5" dioxus-ssr = { workspace = true } reqwest = { workspace = true} +tracing-subscriber = "0.3.18" [dev-dependencies.web-sys] version = "0.3.56" features = [ "Document", "HtmlElement", + "Window" ] [features] -default = [] serialize = ["dep:serde"] [package.metadata.docs.rs] diff --git a/packages/core/README.md b/packages/core/README.md index 07900a3c20..5e348d6ea4 100644 --- a/packages/core/README.md +++ b/packages/core/README.md @@ -18,7 +18,7 @@ loop { 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 { unimplemented!() } fn apply(&self) -> Mutations { Mutations::default() } } # }); ``` diff --git a/packages/core/src/any_props.rs b/packages/core/src/any_props.rs index dadcec4797..bcb8631db5 100644 --- a/packages/core/src/any_props.rs +++ b/packages/core/src/any_props.rs @@ -1,8 +1,4 @@ -use crate::{ - innerlude::{throw_error, CapturedPanic}, - nodes::RenderReturn, - ComponentFunction, -}; +use crate::{innerlude::CapturedPanic, nodes::RenderReturn, ComponentFunction}; use std::{any::Any, panic::AssertUnwindSafe}; pub(crate) type BoxedAnyProps = Box; @@ -15,6 +11,8 @@ pub(crate) trait AnyProps: 'static { fn memoize(&mut self, other: &dyn Any) -> bool; /// Get the props as a type erased `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. fn duplicate(&self) -> BoxedAnyProps; } @@ -72,19 +70,24 @@ impl + Clone, P: Clone + 'static, M: 'static> AnyProp &self.props } + fn props_mut(&mut self) -> &mut dyn Any { + &mut self.props + } + fn render(&self) -> RenderReturn { let res = std::panic::catch_unwind(AssertUnwindSafe(move || { self.render_fn.rebuild(self.props.clone()) })); match res { - Ok(Some(e)) => RenderReturn::Ready(e), - Ok(None) => RenderReturn::default(), + Ok(node) => RenderReturn { node }, Err(err) => { let component_name = self.name; tracing::error!("Error while rendering component `{component_name}`: {err:?}"); - throw_error::<()>(CapturedPanic { error: err }); - RenderReturn::default() + let panic = CapturedPanic { error: err }; + RenderReturn { + node: Err(panic.into()), + } } } } diff --git a/packages/core/src/arena.rs b/packages/core/src/arena.rs index 0da1290d8e..2a4e955ea6 100644 --- a/packages/core/src/arena.rs +++ b/packages/core/src/arena.rs @@ -19,18 +19,25 @@ pub(crate) struct MountId(pub(crate) usize); impl Default for MountId { fn default() -> Self { - Self(usize::MAX) + Self::PLACEHOLDER } } impl MountId { + pub(crate) const PLACEHOLDER: Self = Self(usize::MAX); + pub(crate) fn as_usize(self) -> Option { - if self.0 == usize::MAX { + if self == Self::PLACEHOLDER { None } else { Some(self.0) } } + + #[allow(unused)] + pub(crate) fn mounted(self) -> bool { + self != Self::PLACEHOLDER + } } #[derive(Debug, Clone, Copy)] @@ -53,16 +60,18 @@ impl VirtualDom { } pub(crate) fn reclaim(&mut self, el: ElementId) { - self.try_reclaim(el) - .unwrap_or_else(|| panic!("cannot reclaim {:?}", el)); + if !self.try_reclaim(el) { + tracing::error!("cannot reclaim {:?}", el); + } } - pub(crate) fn try_reclaim(&mut self, el: ElementId) -> Option<()> { - if el.0 == 0 { - panic!("Cannot reclaim the root element",); + pub(crate) fn try_reclaim(&mut self, el: ElementId) -> bool { + // We never reclaim the unmounted elements or 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 diff --git a/packages/core/src/diff/component.rs b/packages/core/src/diff/component.rs index 2b75fe40ba..6d6e168c78 100644 --- a/packages/core/src/diff/component.rs +++ b/packages/core/src/diff/component.rs @@ -1,56 +1,125 @@ -use std::ops::{Deref, DerefMut}; +use std::{ + any::TypeId, + ops::{Deref, DerefMut}, +}; use crate::{ any_props::AnyProps, - innerlude::{ElementRef, MountId, ScopeOrder, VComponent, WriteMutations}, - nodes::RenderReturn, + innerlude::{ + ElementRef, MountId, ScopeOrder, SuspenseBoundaryProps, SuspenseBoundaryPropsWithOwner, + VComponent, WriteMutations, + }, nodes::VNode, scopes::ScopeId, virtual_dom::VirtualDom, + RenderReturn, }; impl VirtualDom { - pub(crate) fn diff_scope( + pub(crate) fn run_and_diff_scope( &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( + &mut self, + to: Option<&mut M>, scope: ScopeId, 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]; - // Load the old and new bump arenas - let new = &new_nodes; + // Load the old and new rendered nodes 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]; - scope_state.last_rendered_node = Some(new_nodes); + self.scopes[scope.0].last_rendered_node = Some(new_nodes); + + if render_to.is_some() { + self.runtime.get_state(scope).unwrap().mount(&self.runtime); + } - self.runtime.scope_stack.borrow_mut().pop(); + 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. - pub(crate) fn create_scope( + /// Returns the number of nodes created on the stack + #[tracing::instrument(skip(self, to), level = "trace", name = "VirtualDom::create_scope")] + pub(crate) fn create_scope( &mut self, - to: &mut impl WriteMutations, + to: Option<&mut M>, scope: ScopeId, - new_node: RenderReturn, + new_nodes: RenderReturn, parent: Option, ) -> 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 - 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 - self.scopes[scope.0].last_rendered_node = Some(new_node); + self.scopes[scope.0].last_rendered_node = Some(new_nodes); + + if render_to.is_some() { + self.runtime.get_state(scope).unwrap().mount(&self.runtime); + } - self.runtime.scope_stack.borrow_mut().pop(); + self.runtime.pop_scope(); nodes } + + pub(crate) fn remove_component_node( + &mut self, + to: Option<&mut M>, + destroy_component_state: bool, + scope_id: ScopeId, + replace_with: Option, + ) { + // 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::(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 { @@ -63,7 +132,7 @@ impl VNode { scope_id: ScopeId, parent: Option, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: Option<&mut impl WriteMutations>, ) { // Replace components that have different render fns if old.render_fn != new.render_fn { @@ -83,9 +152,8 @@ impl VNode { return; } - // Now run the component and diff it - let new = dom.run_scope(scope_id); - dom.diff_scope(to, scope_id, new); + // Now diff the scope + dom.run_and_diff_scope(to, scope_id); let height = dom.runtime.get_state(scope_id).unwrap().height; dom.dirty_scopes.remove(&ScopeOrder::new(height, scope_id)); @@ -98,16 +166,21 @@ impl VNode { new: &VComponent, parent: Option, 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 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 - 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( &self, mount: MountId, @@ -115,19 +188,40 @@ impl VNode { component: &VComponent, parent: Option, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: Option<&mut impl WriteMutations>, ) -> usize { - // Load up a ScopeId for this vcomponent. If it's already mounted, then we can just use that - let scope = dom - .new_scope(component.props.duplicate(), component.name) - .state() - .id; + // If this is a suspense boundary, run our suspense creation logic instead of running the component + if component.props.props().type_id() == TypeId::of::() { + return SuspenseBoundaryProps::create(mount, idx, component, parent, dom, to); + } + + 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() { + scope_id = dom + .new_scope(component.props.duplicate(), component.name) + .state() + .id; + + // Store the scope id for the next render + dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope_id.0; - // Store the scope id for the next render - dom.mounts[mount.0].mounted_dynamic_nodes[idx] = scope.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 = dom.run_scope(scope); + let new_node = dom.scopes[scope.0] + .last_rendered_node + .as_ref() + .expect("Component to be mounted") + .clone(); - dom.create_scope(to, scope, new, parent) + dom.create_scope(to, scope, new_node, parent) } } diff --git a/packages/core/src/diff/iterator.rs b/packages/core/src/diff/iterator.rs index f40fbad8c7..497526c93b 100644 --- a/packages/core/src/diff/iterator.rs +++ b/packages/core/src/diff/iterator.rs @@ -1,7 +1,7 @@ use crate::{ innerlude::{ElementRef, WriteMutations}, nodes::VNode, - DynamicNode, ScopeId, TemplateNode, VirtualDom, + DynamicNode, ScopeId, VirtualDom, }; use rustc_hash::{FxHashMap, FxHashSet}; @@ -9,7 +9,7 @@ use rustc_hash::{FxHashMap, FxHashSet}; impl VirtualDom { pub(crate) fn diff_non_empty_fragment( &mut self, - to: &mut impl WriteMutations, + to: Option<&mut impl WriteMutations>, old: &[VNode], new: &[VNode], parent: Option, @@ -42,7 +42,7 @@ impl VirtualDom { // the change list stack is in the same state when this function returns. fn diff_non_keyed_children( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, old: &[VNode], new: &[VNode], parent: Option, @@ -54,15 +54,18 @@ impl VirtualDom { debug_assert!(!old.is_empty()); match old.len().cmp(&new.len()) { - Ordering::Greater => self.remove_nodes(to, &old[new.len()..], None), - Ordering::Less => { - self.create_and_insert_after(to, &new[old.len()..], old.last().unwrap(), parent) - } + Ordering::Greater => self.remove_nodes(to.as_deref_mut(), &old[new.len()..], None), + Ordering::Less => self.create_and_insert_after( + to.as_deref_mut(), + &new[old.len()..], + old.last().unwrap(), + parent, + ), Ordering::Equal => {} } 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. fn diff_keyed_children( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, old: &[VNode], new: &[VNode], parent: Option, @@ -116,10 +119,11 @@ impl VirtualDom { // // `shared_prefix_count` is the count of how many nodes at the start of // `new` and `old` share the same keys. - let (left_offset, right_offset) = match self.diff_keyed_ends(to, old, new, parent) { - Some(count) => count, - None => return, - }; + let (left_offset, right_offset) = + match self.diff_keyed_ends(to.as_deref_mut(), old, new, parent) { + Some(count) => count, + None => return, + }; // 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 @@ -164,7 +168,7 @@ impl VirtualDom { /// If there is no offset, then this function returns None and the diffing is complete. fn diff_keyed_ends( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, old: &[VNode], new: &[VNode], parent: Option, @@ -176,7 +180,7 @@ impl VirtualDom { if old.key != new.key { break; } - old.diff_node(new, self, to); + old.diff_node(new, self, to.as_deref_mut()); left_offset += 1; } @@ -201,7 +205,7 @@ impl VirtualDom { if old.key != new.key { break; } - old.diff_node(new, self, to); + old.diff_node(new, self, to.as_deref_mut()); right_offset += 1; } @@ -224,7 +228,7 @@ impl VirtualDom { #[allow(clippy::too_many_lines)] fn diff_keyed_middle( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, old: &[VNode], new: &[VNode], parent: Option, @@ -236,7 +240,7 @@ impl VirtualDom { - 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 - 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 - 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. // 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 .iter() .enumerate() - .map(|(i, o)| (o.key.as_ref().unwrap(), i)) + .map(|(i, o)| (o.key.as_ref().unwrap().as_str(), i)) .collect::>(); let mut shared_keys = FxHashSet::default(); @@ -271,163 +275,194 @@ impl VirtualDom { .iter() .map(|node| { 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); index } else { - u32::MAX as usize + usize::MAX } }) - .collect::>(); + .collect::>(); // 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. if shared_keys.is_empty() { - if !old.is_empty() { - let m = self.create_children(to, new, parent); - self.remove_nodes(to, old, Some(m)); - } else { - // I think this is wrong - why are we appending? - // only valid of the if there are no trailing elements - // self.create_and_append_children(new); + debug_assert!( + !old.is_empty(), + "we should never be appending - just creating N" + ); + + let m = self.create_children(to.as_deref_mut(), new, parent); + self.remove_nodes(to, old, Some(m)); - todo!("we should never be appending - just creating N"); - } return; } // remove any old children that are not shared - // todo: make this an iterator - for child in old { - let key = child.key.as_ref().unwrap(); - if !shared_keys.contains(&key) { - child.remove_node(self, to, None, true); - } + for child_to_remove in old + .iter() + .filter(|child| !shared_keys.contains(child.key.as_ref().unwrap())) + { + child_to_remove.remove_node(self, to.as_deref_mut(), None); } // 4. Compute the LIS of this list 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 starts = vec![0; new_index_to_old_index.len()]; + let mut allocation = vec![0; new_index_to_old_index.len() * 2]; + let (predecessors, starts) = allocation.split_at_mut(new_index_to_old_index.len()); longest_increasing_subsequence::lis_with( &new_index_to_old_index, &mut lis_sequence, |a, b| a < b, - &mut predecessors, - &mut starts, + predecessors, + 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 lis_sequence.last().map(|f| new_index_to_old_index[*f]) == Some(u32::MAX as usize) { - lis_sequence.pop(); + if lis_sequence.first().map(|f| new_index_to_old_index[*f]) == Some(usize::MAX) { + lis_sequence.remove(0); } + // Diff each nod in the LIS 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, + new_index_to_old_index: &[usize], + range: std::ops::Range, + ) -> 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 - let last = *lis_sequence.last().unwrap(); + // add mount instruction for the items before the LIS + let last = *lis_sequence.first().unwrap(); if last < (new.len() - 1) { - for (idx, new_node) in new[(last + 1)..].iter().enumerate() { - let new_idx = idx + last + 1; - let old_index = new_index_to_old_index[new_idx]; - if old_index == u32::MAX as usize { - nodes_created += new_node.create(self, to, parent); - } else { - old[old_index].diff_node(new_node, self, to); - nodes_created += new_node.push_all_real_nodes(self, to); - } - } - - let id = new[last].find_last_element(self); - if nodes_created > 0 { - to.insert_nodes_after(id, nodes_created) - } - nodes_created = 0; + let nodes_created = create_or_diff( + self, + new, + old, + to.as_deref_mut(), + parent, + &new_index_to_old_index, + (last + 1)..new.len(), + ); + + // Insert all the nodes that we just created after the last node in the LIS + self.insert_after(to.as_deref_mut(), nodes_created, &new[last]); } - // for each spacing, generate a mount instruction - let mut lis_iter = lis_sequence.iter().rev(); + // For each node inside of the LIS, but not included in the LIS, generate a mount instruction + // 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(); for next in lis_iter { if last - next > 1 { - for (idx, new_node) in new[(next + 1)..last].iter().enumerate() { - let new_idx = idx + next + 1; - let old_index = new_index_to_old_index[new_idx]; - if old_index == u32::MAX as usize { - nodes_created += new_node.create(self, to, parent); - } else { - old[old_index].diff_node(new_node, self, to); - nodes_created += new_node.push_all_real_nodes(self, to); - } - } - - let id = new[last].find_first_element(self); - if nodes_created > 0 { - to.insert_nodes_before(id, nodes_created); - } + let nodes_created = create_or_diff( + self, + new, + old, + to.as_deref_mut(), + parent, + &new_index_to_old_index, + (next + 1)..last, + ); - nodes_created = 0; + self.insert_before(to.as_deref_mut(), nodes_created, &new[last]); } last = *next; } - // add mount instruction for the last items not covered by the lis - let first_lis = *lis_sequence.first().unwrap(); + // add mount instruction for the items after the LIS + let first_lis = *lis_sequence.last().unwrap(); if first_lis > 0 { - for (idx, new_node) in new[..first_lis].iter().enumerate() { - let old_index = new_index_to_old_index[idx]; - if old_index == u32::MAX as usize { - nodes_created += new_node.create(self, to, parent); - } else { - old[old_index].diff_node(new_node, self, to); - nodes_created += new_node.push_all_real_nodes(self, to); - } - } - - let id = new[first_lis].find_first_element(self); - if nodes_created > 0 { - to.insert_nodes_before(id, nodes_created); - } + let nodes_created = create_or_diff( + self, + new, + old, + to.as_deref_mut(), + parent, + &new_index_to_old_index, + 0..first_lis, + ); + + self.insert_before(to, nodes_created, &new[first_lis]); } } fn create_and_insert_before( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, new: &[VNode], before: &VNode, parent: Option, ) { - let m = self.create_children(to, new, parent); - let id = before.find_first_element(self); - to.insert_nodes_before(id, m); + let m = self.create_children(to.as_deref_mut(), new, parent); + self.insert_before(to, m, before); + } + + 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( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, new: &[VNode], after: &VNode, parent: Option, ) { - let m = self.create_children(to, new, parent); - let id = after.find_last_element(self); - to.insert_nodes_after(id, m); + let m = self.create_children(to.as_deref_mut(), new, parent); + self.insert_after(to, m, after); + } + + 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 { - /// Push all the real nodes on the stack - pub(crate) fn push_all_real_nodes( + /// Push all the root nodes on the stack + pub(crate) fn push_all_root_nodes( &self, dom: &VirtualDom, to: &mut impl WriteMutations, @@ -440,30 +475,27 @@ impl VNode { .roots .iter() .enumerate() - .map(|(root_idx, _)| match &self.template.get().roots[root_idx] { - TemplateNode::Dynamic { id: idx } => match &self.dynamic_nodes[*idx] { - DynamicNode::Placeholder(_) | DynamicNode::Text(_) => { - to.push_root(mount.root_ids[root_idx]); - 1 - } - DynamicNode::Fragment(nodes) => { + .map( + |(root_idx, _)| match self.get_dynamic_root_node_and_id(root_idx) { + Some((_, DynamicNode::Fragment(nodes))) => { let mut accumulated = 0; for node in nodes { - accumulated += node.push_all_real_nodes(dom, to); + accumulated += node.push_all_root_nodes(dom, to); } accumulated } - DynamicNode::Component(_) => { - let scope = ScopeId(mount.mounted_dynamic_nodes[*idx]); + Some((idx, DynamicNode::Component(_))) => { + let scope = ScopeId(mount.mounted_dynamic_nodes[idx]); 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() } } diff --git a/packages/core/src/diff/mod.rs b/packages/core/src/diff/mod.rs index 9f439d15a5..6697936ea8 100644 --- a/packages/core/src/diff/mod.rs +++ b/packages/core/src/diff/mod.rs @@ -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)] use crate::{ arena::ElementId, innerlude::{ElementRef, MountId, WriteMutations}, nodes::VNode, - scopes::ScopeId, virtual_dom::VirtualDom, Template, TemplateNode, }; @@ -14,34 +22,36 @@ mod iterator; mod node; impl VirtualDom { - pub(crate) fn create_children<'a>( + pub(crate) fn create_children( &mut self, - to: &mut impl WriteMutations, - nodes: impl IntoIterator, + mut to: Option<&mut impl WriteMutations>, + nodes: &[VNode], parent: Option, ) -> usize { nodes - .into_iter() - .map(|child| child.create(self, to, parent)) + .iter() + .map(|child| child.create(self, parent, to.as_deref_mut())) .sum() } /// Simply replace a placeholder with a list of nodes - fn replace_placeholder<'a>( + fn replace_placeholder( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, placeholder_id: ElementId, - r: impl IntoIterator, + r: &[VNode], parent: Option, ) { - let m = self.create_children(to, r, parent); - to.replace_node_with(placeholder_id, m); - self.reclaim(placeholder_id); + let m = self.create_children(to.as_deref_mut(), r, parent); + if let Some(to) = to { + to.replace_node_with(placeholder_id, m); + self.reclaim(placeholder_id); + } } fn nodes_to_placeholder( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, mount: MountId, dyn_node_idx: usize, old_nodes: &[VNode], @@ -52,13 +62,15 @@ impl VirtualDom { // Set the id of the placeholder 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); } /// 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!( !nodes.is_empty(), "replace_nodes must have at least one node" @@ -73,32 +85,16 @@ impl VirtualDom { /// Wont generate mutations for the inner nodes fn remove_nodes( &mut self, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, nodes: &[VNode], replace_with: Option, ) { for (i, node) in nodes.iter().rev().enumerate() { 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, - 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 // used in conditional compilation #[allow(unused_mut)] @@ -107,40 +103,37 @@ impl VirtualDom { to: &mut impl WriteMutations, mut template: Template, ) { - let (path, byte_index) = template.name.rsplit_once(':').unwrap(); - - let byte_index = byte_index.parse::().unwrap(); - // First, check if we've already seen this template - if self - .templates - .get(&path) - .filter(|set| set.contains_key(&byte_index)) - .is_none() + // In debug mode, we check the more complete hashmap by byte index + #[cfg(debug_assertions)] { - // if hot reloading is enabled, then we need to check for a template that has overriten this one - #[cfg(debug_assertions)] - if let Some(mut new_template) = self - .templates - .get_mut(path) - .and_then(|map| map.remove(&usize::MAX)) - { - // the byte index of the hot reloaded template could be different - new_template.name = template.name; - template = new_template; + let (path, byte_index) = template.name.rsplit_once(':').unwrap(); + + let byte_index = byte_index.parse::().unwrap(); + let mut entry = self.templates.entry(path); + // If we've already seen this template, just return + if let std::collections::hash_map::Entry::Occupied(occupied) = &entry { + if occupied.get().contains_key(&byte_index) { + return; + } } - self.templates - .entry(path) - .or_default() - .insert(byte_index, template); + // Otherwise, insert it and register it + entry.or_default().insert(byte_index, template); + } - // If it's all dynamic nodes, then we don't need to register it - if !template.is_completely_dynamic() { - to.register_template(template) - } + // In release mode, everything is built into the &'static str + #[cfg(not(debug_assertions))] + 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 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 diff --git a/packages/core/src/diff/node.rs b/packages/core/src/diff/node.rs index ff54ba869e..d27450927c 100644 --- a/packages/core/src/diff/node.rs +++ b/packages/core/src/diff/node.rs @@ -1,5 +1,5 @@ use crate::innerlude::MountId; -use crate::{Attribute, AttributeValue, DynamicNode::*}; +use crate::{Attribute, AttributeValue, DynamicNode::*, Template}; use crate::{VNode, VirtualDom, WriteMutations}; use core::iter::Peekable; @@ -9,7 +9,6 @@ use crate::{ nodes::DynamicNode, scopes::ScopeId, TemplateNode, - TemplateNode::*, }; impl VNode { @@ -17,10 +16,10 @@ impl VNode { &self, new: &VNode, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, ) { // The node we are diffing from should always be mounted - debug_assert!(dom.mounts.get(self.mount.get().0).is_some()); + debug_assert!(dom.mounts.get(self.mount.get().0).is_some() || to.is_none()); // If hot reloading is enabled, we need to make sure we're using the latest template #[cfg(debug_assertions)] @@ -33,7 +32,7 @@ impl VNode { if template != self.template.get() { let mount_id = self.mount.get(); let parent = dom.mounts[mount_id.0].parent; - self.replace([new], parent, dom, to); + self.replace(std::slice::from_ref(new), parent, dom, to); return; } } @@ -56,16 +55,20 @@ impl VNode { // If the templates are the same, we can diff the attributes and children // Start with the attributes - self.diff_attributes(new, dom, to); + // Since the attributes are only side effects, we can skip diffing them entirely if the node is suspended and we aren't outputting mutations + if let Some(to) = to.as_deref_mut() { + self.diff_attributes(new, dom, to); + } // Now diff the dynamic nodes - self.dynamic_nodes + for (dyn_node_idx, (old, new)) in self + .dynamic_nodes .iter() .zip(new.dynamic_nodes.iter()) .enumerate() - .for_each(|(dyn_node_idx, (old, new))| { - self.diff_dynamic_node(mount_id, dyn_node_idx, old, new, dom, to) - }); + { + self.diff_dynamic_node(mount_id, dyn_node_idx, old, new, dom, to.as_deref_mut()) + } } fn move_mount_to(&self, new: &VNode, dom: &mut VirtualDom) { @@ -73,10 +76,12 @@ impl VNode { let mount_id = self.mount.get(); new.mount.set(mount_id); - let mount = &mut dom.mounts[mount_id.0]; + if mount_id.mounted() { + let mount = &mut dom.mounts[mount_id.0]; - // Update the reference to the node for bubbling events - mount.node = new.clone_mounted(); + // Update the reference to the node for bubbling events + mount.node = new.clone_mounted(); + } } fn diff_dynamic_node( @@ -86,28 +91,26 @@ impl VNode { old_node: &DynamicNode, new_node: &DynamicNode, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: Option<&mut impl WriteMutations>, ) { - let parent = || ElementRef { - mount, - path: ElementPath { - path: self.template.get().node_paths[idx], - }, - }; + tracing::trace!("diffing dynamic node from {old_node:?} to {new_node:?}"); match (old_node, new_node) { (Text(old), Text(new)) => { - let mount = &dom.mounts[mount.0]; - self.diff_vtext( to, mount, idx, old, new) + // Diffing text is just a side effect, if we are diffing suspended nodes and are not outputting mutations, we can skip it + if let Some(to) = to{ + let mount = &dom.mounts[mount.0]; + self.diff_vtext(to, mount, idx, old, new) + } }, (Placeholder(_), Placeholder(_)) => {}, - (Fragment(old), Fragment(new)) => dom.diff_non_empty_fragment(to, old, new, Some(parent())), + (Fragment(old), Fragment(new)) => dom.diff_non_empty_fragment(to, old, new, Some(self.reference_to_dynamic_node(mount, idx))), (Component(old), Component(new)) => { let scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); - self.diff_vcomponent(mount, idx, new, old, scope_id, Some(parent()), dom, to) + self.diff_vcomponent(mount, idx, new, old, scope_id, Some(self.reference_to_dynamic_node(mount, idx)), dom, to) }, (Placeholder(_), Fragment(right)) => { let placeholder_id = ElementId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); - dom.replace_placeholder(to, placeholder_id, right, Some(parent()))}, + dom.replace_placeholder(to, placeholder_id, right, Some(self.reference_to_dynamic_node(mount, idx)))}, (Fragment(left), Placeholder(_)) => { dom.nodes_to_placeholder(to, mount, idx, left,) }, @@ -115,47 +118,55 @@ impl VNode { }; } + /// Try to get the dynamic node and its index for a root node + pub(crate) fn get_dynamic_root_node_and_id( + &self, + root_idx: usize, + ) -> Option<(usize, &DynamicNode)> { + self.template.get().roots[root_idx] + .dynamic_id() + .map(|id| (id, &self.dynamic_nodes[id])) + } + pub(crate) fn find_first_element(&self, dom: &VirtualDom) -> ElementId { let mount = &dom.mounts[self.mount.get().0]; - match &self.template.get().roots[0] { - TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => mount.root_ids[0], - TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { - match &self.dynamic_nodes[*id] { - Placeholder(_) | Text(_) => ElementId(mount.mounted_dynamic_nodes[*id]), - Fragment(children) => { - let child = children.first().unwrap(); - child.find_first_element(dom) - } - Component(_comp) => { - let scope = ScopeId(mount.mounted_dynamic_nodes[*id]); - dom.get_scope(scope) - .unwrap() - .root_node() - .find_first_element(dom) - } - } + match self.get_dynamic_root_node_and_id(0) { + // This node is static, just get the root id + None | Some((_, Placeholder(_) | Text(_))) => mount.root_ids[0], + // The node is a fragment, so we need to find the first element in the fragment + Some((_, Fragment(children))) => { + let child = children.first().unwrap(); + child.find_first_element(dom) + } + // The node is a component, so we need to find the first element in the component + Some((id, Component(_))) => { + let scope = ScopeId(mount.mounted_dynamic_nodes[id]); + dom.get_scope(scope) + .unwrap() + .root_node() + .find_first_element(dom) } } } pub(crate) fn find_last_element(&self, dom: &VirtualDom) -> ElementId { let mount = &dom.mounts[self.mount.get().0]; - match &self.template.get().roots.last().unwrap() { - TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => { - *mount.root_ids.last().unwrap() + let last_root_index = self.template.get().roots.len() - 1; + match self.get_dynamic_root_node_and_id(last_root_index) { + // This node is static, just get the root id + None | Some((_, Placeholder(_) | Text(_))) => mount.root_ids[last_root_index], + // The node is a fragment, so we need to find the first element in the fragment + Some((_, Fragment(children))) => { + let child = children.first().unwrap(); + child.find_first_element(dom) } - TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { - match &self.dynamic_nodes[*id] { - Placeholder(_) | Text(_) => ElementId(mount.mounted_dynamic_nodes[*id]), - Fragment(t) => t.last().unwrap().find_last_element(dom), - Component(_comp) => { - let scope = ScopeId(mount.mounted_dynamic_nodes[*id]); - dom.get_scope(scope) - .unwrap() - .root_node() - .find_last_element(dom) - } - } + // The node is a component, so we need to find the first element in the component + Some((id, Component(_))) => { + let scope = ScopeId(mount.mounted_dynamic_nodes[id]); + dom.get_scope(scope) + .unwrap() + .root_node() + .find_last_element(dom) } } } @@ -177,27 +188,65 @@ impl VNode { } } - pub(crate) fn replace<'a>( + pub(crate) fn replace( &self, - right: impl IntoIterator, + right: &[VNode], parent: Option, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: Option<&mut impl WriteMutations>, + ) { + self.replace_inner(right, parent, dom, to, true) + } + + /// Replace this node with new children, but *don't destroy* the old node's component state + /// + /// This is useful for moving a node from the rendered nodes into a suspended node + pub(crate) fn move_node_to_background( + &self, + right: &[VNode], + parent: Option, + dom: &mut VirtualDom, + to: Option<&mut impl WriteMutations>, ) { - let m = dom.create_children(to, right, parent); + self.replace_inner(right, parent, dom, to, false) + } + + pub(crate) fn replace_inner( + &self, + right: &[VNode], + parent: Option, + dom: &mut VirtualDom, + mut to: Option<&mut impl WriteMutations>, + destroy_component_state: bool, + ) { + let m = dom.create_children(to.as_deref_mut(), right, parent); // Instead of *just* removing it, we can use the replace mutation - self.remove_node(dom, to, Some(m), true) + self.remove_node_inner(dom, to, destroy_component_state, Some(m)) } - pub(crate) fn remove_node( + /// Remove a node from the dom and potentially replace it with the top m nodes from the stack + pub(crate) fn remove_node( &self, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: Option<&mut M>, + replace_with: Option, + ) { + self.remove_node_inner(dom, to, true, replace_with) + } + + /// Remove a node, but only maybe destroy the component state of that node. During suspense, we need to remove a node from the real dom without wiping the component state + pub(crate) fn remove_node_inner( + &self, + dom: &mut VirtualDom, + to: Option<&mut M>, + destroy_component_state: bool, replace_with: Option, - gen_muts: bool, ) { let mount = self.mount.get(); + if !mount.mounted() { + return; + } // Clean up any attributes that have claimed a static node as dynamic for mount/unmounts // Will not generate mutations! @@ -206,25 +255,25 @@ impl VNode { // Remove the nested dynamic nodes // We don't generate mutations for these, as they will be removed by the parent (in the next line) // But we still need to make sure to reclaim them from the arena and drop their hooks, etc - self.remove_nested_dyn_nodes(mount, dom, to); + self.remove_nested_dyn_nodes::(mount, dom, destroy_component_state); // Clean up the roots, assuming we need to generate mutations for these // This is done last in order to preserve Node ID reclaim order (reclaim in reverse order of claim) - self.reclaim_roots(mount, dom, to, replace_with, gen_muts); + self.reclaim_roots(mount, dom, to, destroy_component_state, replace_with); - // Remove the mount information - dom.mounts.remove(mount.0); - - tracing::trace!(?self, "removed node"); + if destroy_component_state { + // Remove the mount information + dom.mounts.remove(mount.0); + } } fn reclaim_roots( &self, mount: MountId, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, + destroy_component_state: bool, replace_with: Option, - gen_muts: bool, ) { let roots = self.template.get().roots; for (idx, node) in roots.iter().enumerate() { @@ -234,39 +283,45 @@ impl VNode { self.remove_dynamic_node( mount, dom, - to, + to.as_deref_mut(), + destroy_component_state, id, dynamic_node, replace_with.filter(|_| last_node), - gen_muts, ); - } else { + } else if let Some(to) = to.as_deref_mut() { let mount = &dom.mounts[mount.0]; let id = mount.root_ids[idx]; - if gen_muts { - if let (true, Some(replace_with)) = (last_node, replace_with) { - to.replace_node_with(id, replace_with); - } else { - to.remove_node(id); - } + if let (true, Some(replace_with)) = (last_node, replace_with) { + to.replace_node_with(id, replace_with); + } else { + to.remove_node(id); } dom.reclaim(id); } } } - fn remove_nested_dyn_nodes( + fn remove_nested_dyn_nodes( &self, mount: MountId, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + destroy_component_state: bool, ) { let template = self.template.get(); for (idx, dyn_node) in self.dynamic_nodes.iter().enumerate() { let path_len = template.node_paths.get(idx).map(|path| path.len()); // Roots are cleaned up automatically above and nodes with a empty path are placeholders if let Some(2..) = path_len { - self.remove_dynamic_node(mount, dom, to, idx, dyn_node, None, false) + self.remove_dynamic_node( + mount, + dom, + Option::<&mut M>::None, + destroy_component_state, + idx, + dyn_node, + None, + ) } } } @@ -275,57 +330,59 @@ impl VNode { &self, mount: MountId, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, + destroy_component_state: bool, idx: usize, node: &DynamicNode, replace_with: Option, - gen_muts: bool, ) { match node { Component(_comp) => { let scope_id = ScopeId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); - dom.remove_component_node(to, scope_id, replace_with, gen_muts); + dom.remove_component_node(to, destroy_component_state, scope_id, replace_with); } Text(_) | Placeholder(_) => { let id = ElementId(dom.mounts[mount.0].mounted_dynamic_nodes[idx]); - if gen_muts { + if let Some(to) = to { if let Some(replace_with) = replace_with { to.replace_node_with(id, replace_with); } else { to.remove_node(id); } + dom.reclaim(id) } - dom.reclaim(id) } Fragment(nodes) => { for node in &nodes[..nodes.len() - 1] { - node.remove_node(dom, to, None, gen_muts) + node.remove_node_inner(dom, to.as_deref_mut(), destroy_component_state, None) } if let Some(last_node) = nodes.last() { - last_node.remove_node(dom, to, replace_with, gen_muts) + last_node.remove_node_inner(dom, to, destroy_component_state, replace_with) } } }; } fn templates_are_different(&self, other: &VNode) -> bool { - let self_node_name = self.template.get().name; - let other_node_name = other.template.get().name; - // we want to re-create the node if the template name is different by pointer even if the value is the same so that we can detect when hot reloading changes the template - !std::ptr::eq(self_node_name, other_node_name) + let self_node_name = self.template.get().id(); + let other_node_name = other.template.get().id(); + self_node_name != other_node_name } pub(super) fn reclaim_attributes(&self, mount: MountId, dom: &mut VirtualDom) { + let mut next_id = None; for (idx, path) in self.template.get().attr_paths.iter().enumerate() { // We clean up the roots in the next step, so don't worry about them here if path.len() <= 1 { continue; } - let next_id = dom.mounts[mount.0].mounted_attributes[idx]; - // only reclaim the new element if it's different from the previous one - _ = dom.try_reclaim(next_id); + let new_id = dom.mounts[mount.0].mounted_attributes[idx]; + if Some(new_id) != next_id { + dom.reclaim(new_id); + next_id = Some(new_id); + } } } @@ -484,13 +541,13 @@ impl VNode { &self, new: &VNode, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, ) { let mount_id = self.mount.get(); let mount = &dom.mounts[mount_id.0]; let parent = mount.parent; match matching_components(self, new) { - None => self.replace([new], parent, dom, to), + None => self.replace(std::slice::from_ref(new), parent, dom, to), Some(components) => { self.move_mount_to(new, dom); @@ -505,20 +562,16 @@ impl VNode { scope_id, parent, dom, - to, + to.as_deref_mut(), ) } } } } - /// Create this template and write its mutations - pub(crate) fn create( - &self, - dom: &mut VirtualDom, - to: &mut impl WriteMutations, - parent: Option, - ) -> usize { + /// Get the most up to date template for this rsx block + #[allow(unused)] + pub(crate) fn template(&self, dom: &VirtualDom) -> Template { // check for a overridden template #[cfg(debug_assertions)] { @@ -533,189 +586,170 @@ impl VNode { } }; - let template = self.template.get(); + self.template.get() + } - // The best renderers will have templates pre-hydrated and registered - // Just in case, let's create the template using instructions anyways - dom.register_template(to, template); - - // Initialize the mount information for this template - let entry = dom.mounts.vacant_entry(); - let mount = MountId(entry.key()); - self.mount.set(mount); - tracing::trace!(?self, ?mount, "creating template"); - entry.insert(VNodeMount { - node: self.clone_mounted(), - parent, - root_ids: vec![ElementId(0); template.roots.len()].into_boxed_slice(), - mounted_attributes: vec![ElementId(0); template.attr_paths.len()].into_boxed_slice(), - mounted_dynamic_nodes: vec![0; template.node_paths.len()].into_boxed_slice(), - }); + /// Create this rsx block. This will create scopes from components that this rsx block contains, but it will not write anything to the DOM. + pub(crate) fn create( + &self, + dom: &mut VirtualDom, + parent: Option, + mut to: Option<&mut impl WriteMutations>, + ) -> usize { + // Get the most up to date template + let template = self.template(dom); + + // Initialize the mount information for this vnode if it isn't already mounted + if !self.mount.get().mounted() { + let entry = dom.mounts.vacant_entry(); + let mount = MountId(entry.key()); + self.mount.set(mount); + tracing::trace!(?self, ?mount, "creating template"); + entry.insert(VNodeMount { + node: self.clone_mounted(), + parent, + root_ids: vec![ElementId(0); template.roots.len()].into_boxed_slice(), + mounted_attributes: vec![ElementId(0); template.attr_paths.len()] + .into_boxed_slice(), + mounted_dynamic_nodes: vec![usize::MAX; template.node_paths.len()] + .into_boxed_slice(), + }); + } + + // If we are outputting mutations, mount the node as well + if let Some(to) = to.as_deref_mut() { + // The best renderers will have templates pre-hydrated and registered + // Just in case, let's create the template using instructions anyways + dom.register_template(to, template); + } // Walk the roots, creating nodes and assigning IDs - // nodes in an iterator of ((dynamic_node_index, sorted_index), path) - // todo: adjust dynamic nodes to be in the order of roots and then leaves (ie BFS) - #[cfg(not(debug_assertions))] - let (mut attrs, mut nodes) = ( - template.attr_paths.iter().copied().enumerate().peekable(), - template - .node_paths - .iter() - .copied() - .enumerate() - .map(|(i, path)| ((i, i), path)) - .peekable(), + // nodes in an iterator of (dynamic_node_index, path) + let nodes_sorted = template.breadth_first_node_paths(); + let attrs_sorted = template.breadth_first_attribute_paths(); + + let mut nodes = nodes_sorted.peekable(); + let mut attrs = attrs_sorted.peekable(); + + // Get the mounted id of this block + // At this point, we should have already mounted the block + debug_assert!( + dom.mounts.contains( + self.mount + .get() + .as_usize() + .expect("node should already be mounted"), + ), + "Node mount should be valid" ); + let mount = self.mount.get(); - // If this is a debug build, we need to check that the paths are in the correct order because hot reloading can cause scrambled states - #[cfg(debug_assertions)] - let (attrs_sorted, nodes_sorted) = { - ( - crate::nodes::sort_bfo(template.attr_paths), - crate::nodes::sort_bfo(template.node_paths), - ) - }; - #[cfg(debug_assertions)] - let (mut attrs, mut nodes) = { - ( - attrs_sorted.into_iter().peekable(), - nodes_sorted - .iter() - .copied() - .enumerate() - .map(|(i, (id, path))| ((id, i), path)) - .peekable(), - ) - }; - - template + // Go through each root node and create the node, adding it to the stack. + // Each node already exists in the template, so we can just clone it from the template + let nodes_created = template .roots .iter() .enumerate() - .map(|(idx, root)| match root { - DynamicText { id } | Dynamic { id } => { - nodes.next().unwrap(); - self.write_dynamic_root(mount, *id, dom, to) - } - Element { .. } => { - #[cfg(not(debug_assertions))] - let id = - self.write_element_root(mount, idx, &mut attrs, &mut nodes, &[], dom, to); - #[cfg(debug_assertions)] - let id = self.write_element_root( - mount, - idx, - &mut attrs, - &mut nodes, - &nodes_sorted, - dom, - to, - ); - id + .map(|(root_idx, root)| { + match root { + TemplateNode::Dynamic { id } => { + // Take a dynamic node off the depth first iterator + nodes.next().unwrap(); + // Then mount the node + self.create_dynamic_node(mount, *id, dom, to.as_deref_mut()) + } + // For static text and element nodes, just load the template root. This may be a placeholder or just a static node. We now know that each root node has a unique id + TemplateNode::Text { .. } | TemplateNode::Element { .. } => { + if let Some(to) = to.as_deref_mut() { + self.load_template_root(mount, root_idx, dom, to); + } + + // If this is an element, load in all of the placeholder or dynamic content under this root element too + if matches!(root, TemplateNode::Element { .. }) { + // This operation relies on the fact that the root node is the top node on the stack so we need to do it here + self.load_placeholders( + mount, + &mut nodes, + root_idx as u8, + dom, + to.as_deref_mut(), + ); + // Now write out any attributes we need + if let Some(to) = to.as_deref_mut() { + self.write_attrs(mount, &mut attrs, root_idx as u8, dom, to); + } + } + + // This creates one node on the stack + 1 + } } - TemplateNode::Text { .. } => self.write_static_text_root(mount, idx, dom, to), }) - .sum() + .sum(); + + // And return the number of nodes we created on the stack + nodes_created } } impl VNode { - fn write_static_text_root( - &self, - mount: MountId, - idx: usize, - dom: &mut VirtualDom, - to: &mut impl WriteMutations, - ) -> usize { - // Simply just load the template root, no modifications needed - self.load_template_root(mount, idx, dom, to); - - // Text produces just one node on the stack - 1 + /// Get a reference back into a dynamic node + fn reference_to_dynamic_node(&self, mount: MountId, dynamic_node_id: usize) -> ElementRef { + ElementRef { + path: ElementPath { + path: self.template.get().node_paths[dynamic_node_id], + }, + mount, + } } - fn write_dynamic_root( + pub(crate) fn create_dynamic_node( &self, mount: MountId, - idx: usize, + dynamic_node_id: usize, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + to: Option<&mut impl WriteMutations>, ) -> usize { use DynamicNode::*; - match &self.dynamic_nodes[idx] { + let node = &self.dynamic_nodes[dynamic_node_id]; + match node { Component(component) => { - let parent = Some(ElementRef { - path: ElementPath { - path: self.template.get().node_paths[idx], - }, - mount, - }); - self.create_component_node(mount, idx, component, parent, dom, to) + let parent = Some(self.reference_to_dynamic_node(mount, dynamic_node_id)); + self.create_component_node(mount, dynamic_node_id, component, parent, dom, to) } Fragment(frag) => { - let parent = Some(ElementRef { - path: ElementPath { - path: self.template.get().node_paths[idx], - }, - mount, - }); + let parent = Some(self.reference_to_dynamic_node(mount, dynamic_node_id)); dom.create_children(to, frag, parent) } - Placeholder(_) => { - let id = mount.mount_node(idx, dom); - to.create_placeholder(id); - 1 + Text(text) => { + // If we are diffing suspended nodes and are not outputting mutations, we can skip it + if let Some(to) = to { + self.create_dynamic_text(mount, dynamic_node_id, text, dom, to) + } else { + 0 + } } - Text(VText { value }) => { - let id = mount.mount_node(idx, dom); - to.create_text_node(value, id); - 1 + Placeholder(_) => { + // If we are diffing suspended nodes and are not outputting mutations, we can skip it + if let Some(to) = to { + tracing::trace!("creating placeholder"); + self.create_placeholder(mount, dynamic_node_id, dom, to) + } else { + tracing::trace!("skipping creating placeholder"); + 0 + } } } } - /// We write all the descendent data for this element - /// - /// Elements can contain other nodes - and those nodes can be dynamic or static - /// - /// We want to make sure we write these nodes while on top of the root - fn write_element_root( - &self, - mount: MountId, - root_idx: usize, - dynamic_attrs: &mut Peekable>, - dynamic_nodes_iter: &mut Peekable>, - dynamic_nodes: &[(usize, &'static [u8])], - dom: &mut VirtualDom, - to: &mut impl WriteMutations, - ) -> usize { - // Load the template root and get the ID for the node on the stack - let root_on_stack = self.load_template_root(mount, root_idx, dom, to); - - // Write all the attributes below this root - self.write_attrs_on_root(mount, dynamic_attrs, root_idx as u8, root_on_stack, dom, to); - - // Load in all of the placeholder or dynamic content under this root too - self.load_placeholders( - mount, - dynamic_nodes_iter, - dynamic_nodes, - root_idx as u8, - dom, - to, - ); - - 1 - } - - /// Load all of the placeholder nodes for descendents of this root node + /// Load all of the placeholder nodes for descendent of this root node /// /// ```rust, no_run /// # use dioxus::prelude::*; /// # let some_text = "hello world"; /// # let some_value = "123"; /// rsx! { - /// div { + /// div { // We just wrote this node /// // This is a placeholder /// {some_value} /// @@ -724,64 +758,73 @@ impl VNode { /// } /// }; /// ``` - #[allow(unused)] + /// + /// IMPORTANT: This function assumes that root node is the top node on the stack fn load_placeholders( &self, mount: MountId, - dynamic_nodes_iter: &mut Peekable>, - dynamic_nodes: &[(usize, &'static [u8])], + dynamic_nodes_iter: &mut Peekable>, root_idx: u8, dom: &mut VirtualDom, - to: &mut impl WriteMutations, + mut to: Option<&mut impl WriteMutations>, ) { - let (start, end) = match collect_dyn_node_range(dynamic_nodes_iter, root_idx) { - Some((a, b)) => (a, b), - None => return, - }; - - // If hot reloading is enabled, we need to map the sorted index to the original index of the dynamic node. If it is disabled, we can just use the sorted index - #[cfg(not(debug_assertions))] - let reversed_iter = (start..=end).rev(); - #[cfg(debug_assertions)] - let reversed_iter = (start..=end) - .rev() - .map(|sorted_index| dynamic_nodes[sorted_index].0); - - for idx in reversed_iter { - let m = self.create_dynamic_node(mount, idx, dom, to); - if m > 0 { - // The path is one shorter because the top node is the root - let path = &self.template.get().node_paths[idx][1..]; - to.replace_placeholder_with_nodes(path, m); + // Only take nodes that are under this root node + let from_root_node = |(_, path): &(usize, &[u8])| path.first() == Some(&root_idx); + while let Some((dynamic_node_id, path)) = dynamic_nodes_iter.next_if(from_root_node) { + let m = self.create_dynamic_node(mount, dynamic_node_id, dom, to.as_deref_mut()); + if let Some(to) = to.as_deref_mut() { + // If we actually created real new nodes, we need to replace the placeholder for this dynamic node with the new dynamic nodes + if m > 0 { + // The path is one shorter because the top node is the root + let path = &path[1..]; + to.replace_placeholder_with_nodes(path, m); + } } } } - fn write_attrs_on_root( + /// After we have written a root element, we need to write all the attributes that are on the root node + /// + /// ```rust, ignore + /// rsx! { + /// div { // We just wrote this node + /// class: "{class}", // We need to set these attributes + /// id: "{id}", + /// style: "{style}", + /// } + /// } + /// ``` + /// + /// IMPORTANT: This function assumes that root node is the top node on the stack + fn write_attrs( &self, mount: MountId, - attrs: &mut Peekable>, + dynamic_attrbiutes_iter: &mut Peekable>, root_idx: u8, - root: ElementId, dom: &mut VirtualDom, to: &mut impl WriteMutations, ) { - while let Some((mut attr_id, path)) = - attrs.next_if(|(_, p)| p.first().copied() == Some(root_idx)) + let mut last_path = None; + // Only take nodes that are under this root node + let from_root_node = |(_, path): &(usize, &[u8])| path.first() == Some(&root_idx); + while let Some((attribute_idx, attribute_path)) = + dynamic_attrbiutes_iter.next_if(from_root_node) { - let id = self.assign_static_node_as_dynamic(path, root, dom, to); - - loop { - for attr in &*self.dynamic_attrs[attr_id] { - self.write_attribute(path, attr, id, mount, dom, to); - dom.mounts[mount.0].mounted_attributes[attr_id] = id; + let attribute = &self.dynamic_attrs[attribute_idx]; + let id = match last_path { + // If the last path was exactly the same, we can reuse the id + Some((path, id)) if path == attribute_path => id, + // Otherwise, we need to create a new id + _ => { + let id = self.assign_static_node_as_dynamic(mount, attribute_path, dom, to); + last_path = Some((attribute_path, id)); + id } + }; - // Only push the dynamic attributes forward if they match the current path (same element) - match attrs.next_if(|(_, p)| *p == path) { - Some((next_attr_id, _)) => attr_id = next_attr_id, - None => break, - } + for attr in &**attribute { + self.write_attribute(attribute_path, attr, id, mount, dom, to); + dom.mounts[mount.0].mounted_attributes[attribute_idx] = id; } } } @@ -806,22 +849,22 @@ impl VNode { /// /// That node needs to be loaded at runtime, so we need to give it an ID /// - /// If the node in question is on the stack, we just return that ID + /// If the node in question is the root node, we just return the ID /// /// If the node is not on the stack, we create a new ID for it and assign it fn assign_static_node_as_dynamic( &self, + mount: MountId, path: &'static [u8], - this_id: ElementId, dom: &mut VirtualDom, to: &mut impl WriteMutations, ) -> ElementId { - if path.len() == 1 { - return this_id; + // This is just the root node. We already know it's id + if let [root_idx] = path { + return dom.mounts[mount.0].root_ids[*root_idx as usize]; } - // if attribute is on a root node, then we've already created the element - // Else, it's deep in the template and we should create a new id for it + // The node is deeper in the template and we should create a new id for it let id = dom.next_element(); to.assign_node_id(&path[1..], id); @@ -829,41 +872,8 @@ impl VNode { id } - pub(crate) fn create_dynamic_node( - &self, - mount: MountId, - index: usize, - dom: &mut VirtualDom, - to: &mut impl WriteMutations, - ) -> usize { - use DynamicNode::*; - let node = &self.dynamic_nodes[index]; - match node { - Text(text) => self.create_dynamic_text(mount, index, text, dom, to), - Placeholder(_) => self.create_placeholder(mount, index, dom, to), - Component(component) => { - let parent = Some(ElementRef { - path: ElementPath { - path: self.template.get().node_paths[index], - }, - mount, - }); - self.create_component_node(mount, index, component, parent, dom, to) - } - Fragment(frag) => { - let parent = Some(ElementRef { - path: ElementPath { - path: self.template.get().node_paths[index], - }, - mount, - }); - dom.create_children(to, frag, parent) - } - } - } - /// Mount a root node and return its ID and the path to the node - fn mount_dynamic_node_with_path( + fn create_dynamic_node_with_path( &self, mount: MountId, idx: usize, @@ -886,13 +896,19 @@ impl VNode { dom: &mut VirtualDom, to: &mut impl WriteMutations, ) -> usize { - let (new_id, path) = self.mount_dynamic_node_with_path(mount, idx, dom); - - // Hydrate the text node - to.hydrate_text_node(path, &text.value, new_id); - - // Since we're hydrating an existing node, we don't create any new nodes - 0 + let (new_id, path) = self.create_dynamic_node_with_path(mount, idx, dom); + + // If this is a root node, the path is empty and we need to create a new text node + if path.is_empty() { + to.create_text_node(&text.value, new_id); + // We create one node on the stack + 1 + } else { + // Dynamic text nodes always exist as a placeholder text node in the template, we can just hydrate that text node instead of creating a new one + to.hydrate_text_node(path, &text.value, new_id); + // Since we're hydrating an existing node, we don't create any new nodes + 0 + } } pub(crate) fn create_placeholder( @@ -902,13 +918,19 @@ impl VNode { dom: &mut VirtualDom, to: &mut impl WriteMutations, ) -> usize { - let (id, path) = self.mount_dynamic_node_with_path(mount, idx, dom); - - // Assign the ID to the existing node in the template - to.assign_node_id(path, id); - - // Since the placeholder is already in the DOM, we don't create any new nodes - 0 + let (id, path) = self.create_dynamic_node_with_path(mount, idx, dom); + + // If this is a root node, the path is empty and we need to create a new text node + if path.is_empty() { + to.create_placeholder(id); + // We create one node on the stack + 1 + } else { + // Assign the ID to the existing node in the template + to.assign_node_id(path, id); + // Since the placeholder is already in the DOM, we don't create any new nodes + 0 + } } } @@ -920,30 +942,6 @@ impl MountId { } } -fn collect_dyn_node_range( - dynamic_nodes: &mut Peekable>, - root_idx: u8, -) -> Option<(usize, usize)> { - let start = match dynamic_nodes.peek() { - Some(((_, idx), [first, ..])) if *first == root_idx => *idx, - _ => return None, - }; - - let mut end = start; - - while let Some(((_, idx), p)) = - dynamic_nodes.next_if(|(_, p)| matches!(p, [idx, ..] if *idx == root_idx)) - { - if p.len() == 1 { - continue; - } - - end = idx; - } - - Some((start, end)) -} - fn matching_components<'a>( left: &'a VNode, right: &'a VNode, diff --git a/packages/core/src/effect.rs b/packages/core/src/effect.rs index 96d4c116df..af79b055fd 100644 --- a/packages/core/src/effect.rs +++ b/packages/core/src/effect.rs @@ -16,9 +16,9 @@ pub(crate) struct Effect { } impl Effect { - pub(crate) fn new(order: ScopeOrder, f: impl FnOnce() + 'static) -> Self { + pub(crate) fn new(order: ScopeOrder, f: Box) -> Self { let mut effect = VecDeque::new(); - effect.push_back(Box::new(f) as Box); + effect.push_back(f); Self { order, effect: RefCell::new(effect), diff --git a/packages/core/src/error_boundary.rs b/packages/core/src/error_boundary.rs index a9f4f80442..7c355c2604 100644 --- a/packages/core/src/error_boundary.rs +++ b/packages/core/src/error_boundary.rs @@ -1,21 +1,24 @@ use crate::{ - global_context::{current_scope_id, try_consume_context}, - innerlude::provide_context, - use_hook, Element, IntoDynNode, Properties, ScopeId, Template, TemplateAttribute, TemplateNode, - VNode, + global_context::current_scope_id, innerlude::provide_context, use_hook, Element, IntoDynNode, + Properties, ScopeId, Template, TemplateAttribute, TemplateNode, VNode, }; use std::{ any::{Any, TypeId}, backtrace::Backtrace, - cell::RefCell, + cell::{Ref, RefCell}, error::Error, fmt::{Debug, Display}, rc::Rc, + str::FromStr, }; /// 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. +///
+/// +/// WASM currently does not support caching unwinds, so this struct will not be created in WASM. +/// +///
pub struct CapturedPanic { #[allow(dead_code)] /// The error that was caught @@ -28,288 +31,539 @@ impl Debug for CapturedPanic { } } +impl Display for CapturedPanic { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("Encountered panic: {:?}", self.error)) + } +} + +impl Error for CapturedPanic {} + /// Provide an error boundary to catch errors from child components -pub fn use_error_boundary() -> ErrorBoundary { - use_hook(|| provide_context(ErrorBoundary::new())) +pub fn use_error_boundary() -> ErrorContext { + use_hook(|| provide_context(ErrorContext::new(Vec::new(), current_scope_id().unwrap()))) } -/// A boundary that will capture any errors from child components -#[derive(Debug, Clone, Default)] -pub struct ErrorBoundary { - inner: Rc, +/// 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; } -/// A boundary that will capture any errors from child components -pub struct ErrorBoundaryInner { - error: RefCell>, - _id: ScopeId, +/// 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 From for DisplayError { + fn from(e: E) -> Self { + Self(DisplayErrorInner(Box::new(e))) + } } -impl Debug for ErrorBoundaryInner { +struct DisplayErrorInner(Box); +impl Display for DisplayErrorInner { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ErrorBoundaryInner") - .field("error", &self.error) - .finish() + self.0.fmt(f) } } -/// 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 AnyDebug: Any + Debug { - fn as_any(&self) -> &dyn Any; +impl Debug for DisplayErrorInner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } } -impl AnyDebug for T { +impl Error for DisplayErrorInner {} + +impl AnyError for DisplayError { fn as_any(&self) -> &dyn Any { - self + &self.0 .0 + } + + fn as_error(&self) -> &dyn Error { + &self.0 } } -#[derive(Debug)] -/// An instance of an error captured by a descendant component. -pub struct CapturedError { - /// The error captured by the error boundary - pub error: Box, +/// 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: 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::().show(|error| rsx! { + /// div { + /// background_color: "red", + /// color: "white", + /// "Error parsing number: {error}" + /// } + /// })?; + /// todo!() + /// } + /// ``` + fn show(self, display_error: impl FnOnce(&E) -> Element) -> Result; - /// The backtrace of the error - pub backtrace: Backtrace, + /// 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::().context("Parsing number inside of the NumberParser")?; + /// todo!() + /// } + /// ``` + fn context(self, context: C) -> Result; - /// The scope that threw the error - pub scope: ScopeId, + /// 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::().with_context(|| format!("Timestamp: {:?}", std::time::Instant::now()))?; + /// todo!() + /// } + /// ``` + fn with_context(self, context: impl FnOnce() -> C) -> Result; } -impl Display for CapturedError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!( - "Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}", - self.error, self.scope, self.backtrace - )) +impl Context for std::result::Result +where + E: Error + 'static, +{ + fn show(self, display_error: impl FnOnce(&E) -> Element) -> Result { + // 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) + } + } } -} -impl Error for CapturedError {} + fn context(self, context: C) -> Result { + self.with_context(|| context) + } -impl CapturedError { - /// Downcast the error type into a concrete error type - pub fn downcast(&self) -> Option<&T> { - if TypeId::of::() == (*self.error).type_id() { - self.error.as_any().downcast_ref::() - } else { - None + fn with_context(self, context: impl FnOnce() -> C) -> Result { + // 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 Default for ErrorBoundaryInner { - fn default() -> Self { - Self { - error: RefCell::new(None), - _id: current_scope_id() - .expect("Cannot create an error boundary outside of a component's scope."), +impl Context for Option { + fn show(self, display_error: impl FnOnce(&CapturedError) -> Element) -> Result { + // 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(self, context: C) -> Result { + self.with_context(|| context) + } + + fn with_context(self, context: impl FnOnce() -> C) -> Result { + // 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) + } } } } -impl ErrorBoundary { - /// Create a new error boundary - pub fn new() -> Self { - Self::default() +pub(crate) mod private { + use super::*; + + pub trait Sealed {} + + impl Sealed for std::result::Result where E: Error {} + impl Sealed for Option {} +} + +impl AnyError for T { + fn as_any(&self) -> &dyn Any { + self + } + + fn as_error(&self) -> &dyn Error { + self + } +} + +/// A context with information about suspended components +#[derive(Debug, Clone)] +pub struct ErrorContext { + errors: Rc>>, + id: ScopeId, +} + +impl PartialEq for ErrorContext { + fn eq(&self, other: &Self) -> bool { + Rc::ptr_eq(&self.errors, &other.errors) } +} - /// Create a new error boundary in the current scope - pub(crate) fn new_in_scope(scope: ScopeId) -> Self { +impl ErrorContext { + /// Create a new suspense boundary in a specific scope + pub(crate) fn new(errors: Vec, id: ScopeId) -> Self { Self { - inner: Rc::new(ErrorBoundaryInner { - error: RefCell::new(None), - _id: scope, - }), + 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 { + self.errors.borrow().iter().find_map(|task| task.show()) + } + /// 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, + 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, + scope: Option, +} + +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, - })); - if self.inner._id != ScopeId::ROOT { - self.inner._id.needs_update(); + context, + scope, + } = self; + + write!(f, "{context} (from ")?; + + if let Some(scope) = scope { + write!(f, "scope {scope:?} ")?; } - } - /// Take any error that has been captured by this error boundary - pub fn take_error(&self) -> Option { - self.inner.error.take() + write!(f, "at {backtrace:?})") } } -/// 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: Sized { - /// The value that will be returned in if the given value is `Ok`. - type Out; +/// 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` everywhere +pub type Result = std::result::Result; - /// 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; +/// 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` everywhere +#[allow(non_snake_case)] +pub fn Ok(value: T) -> Result { + Result::Ok(value) +} - /// 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(self, e: impl FnOnce() -> D) -> Option { - self.throw().or_else(|| throw_error(e())) +#[derive(Clone)] +/// An instance of an error captured by a descendant component. +pub struct CapturedError { + /// The error captured by the error boundary + error: Rc, + + /// The backtrace of the error + backtrace: Rc, + + /// The scope that threw the error + 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>, +} + +impl FromStr for CapturedError { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> std::result::Result { + std::result::Result::Ok(Self::from_display(s.to_string())) } } -pub(crate) fn throw_error(e: impl Debug + 'static) -> Option { - if let Some(cx) = try_consume_context::() { - 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.") - } - } +#[cfg(feature = "serialize")] +#[derive(serde::Serialize, serde::Deserialize)] +struct SerializedCapturedError { + error: String, + context: Vec, +} + +#[cfg(feature = "serialize")] +impl serde::Serialize for CapturedError { + fn serialize( + &self, + serializer: S, + ) -> std::result::Result { + let serialized = SerializedCapturedError { + error: self.error.as_error().to_string(), + context: self + .context + .iter() + .map(|context| context.to_string()) + .collect(), + }; + serialized.serialize(serializer) } +} - None +#[cfg(feature = "serialize")] +impl<'de> serde::Deserialize<'de> for CapturedError { + fn deserialize>( + deserializer: D, + ) -> std::result::Result { + 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(), + }) + } } -/// We call clone on any errors that can be owned out of a reference -impl<'a, T, O: Debug + 'static, E: ToOwned> Throw for &'a Result { - type Out = &'a T; +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() + } +} - fn throw(self) -> Option { - match self { - Ok(t) => Some(t), - Err(e) => throw_error(e.to_owned()), +impl From 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(), } } +} - fn throw_with(self, err: impl FnOnce() -> D) -> Option { - match self { - Ok(t) => Some(t), - Err(_e) => throw_error(err()), +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(), } } -} -/// Or just throw errors we know about -impl Throw for Result { - type Out = T; + /// 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(), + } + } - fn throw(self) -> Option { - match self { - Ok(t) => Some(t), - Err(e) => throw_error(e), + /// 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 { + if self.render == VNode::placeholder() { + None + } else { + Some(std::result::Result::Ok(self.render.clone())) } } +} - fn throw_with(self, error: impl FnOnce() -> D) -> Option { - self.ok().or_else(|| throw_error(error())) +impl PartialEq for CapturedError { + fn eq(&self, other: &Self) -> bool { + format!("{:?}", self) == format!("{:?}", other) } } -/// Or just throw errors we know about -impl Throw for Option { - type Out = T; +impl Display for CapturedError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "Encountered error: {:?}\nIn scope: {:?}\nBacktrace: {}\nContext: ", + self.error.as_error(), + self.scope, + self.backtrace + ))?; + for context in &*self.context { + f.write_fmt(format_args!("{}\n", context))?; + } + std::result::Result::Ok(()) + } +} - fn throw(self) -> Option { - self.or_else(|| throw_error("Attempted to unwrap a None value.")) +impl CapturedError { + /// Downcast the error type into a concrete error type + pub fn downcast(&self) -> Option<&T> { + if TypeId::of::() == (*self.error).type_id() { + self.error.as_any().downcast_ref::() + } else { + None + } } +} - fn throw_with(self, error: impl FnOnce() -> D) -> Option { - self.or_else(|| throw_error(error())) +pub(crate) fn throw_into(error: impl Into, scope: ScopeId) { + let error = error.into(); + if let Some(cx) = scope.consume_context::() { + cx.insert_error(error) + } else { + tracing::error!( + "Tried to throw an error into an error boundary, but failed to locate a boundary: {:?}", + error + ) } } +#[allow(clippy::type_complexity)] #[derive(Clone)] -pub struct ErrorHandler(Rc Element>); -impl Element + 'static> From for ErrorHandler { +pub struct ErrorHandler(Rc Element>); +impl Element + 'static> From for ErrorHandler { fn from(value: F) -> Self { Self(Rc::new(value)) } } -fn default_handler(error: CapturedError) -> Element { + +fn default_handler(errors: ErrorContext) -> Element { static TEMPLATE: Template = Template { name: "error_handle.rs:42:5:884", roots: &[TemplateNode::Element { - tag: "pre", + tag: "div", namespace: None, attrs: &[TemplateAttribute::Static { name: "color", namespace: Some("style"), value: "red", }], - children: &[TemplateNode::DynamicText { id: 0usize }], + children: &[TemplateNode::Dynamic { id: 0usize }], }], node_paths: &[&[0u8, 0u8]], attr_paths: &[], }; - Some(VNode::new( + std::result::Result::Ok(VNode::new( None, 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(), )) } @@ -433,9 +687,7 @@ impl< { pub fn build(self) -> ErrorBoundaryProps { let (children, handle_error) = self.fields; - let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, || { - ::core::default::Default::default() - }); + let children = ErrorBoundaryPropsBuilder_Optional::into_value(children, VNode::empty); let handle_error = ErrorBoundaryPropsBuilder_Optional::into_value(handle_error, || { 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 /// -/// 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 /// -/// ```rust +/// ```rust, no_run /// # use dioxus::prelude::*; -/// # fn ThrowsError() -> Element { unimplemented!() } -/// rsx! { -/// ErrorBoundary { -/// handle_error: |error| rsx! { "Oops, we encountered an error. Please report {error} to the developer of this application" }, -/// ThrowsError {} +/// fn App() -> Element { +/// rsx! { +/// ErrorBoundary { +/// handle_error: |errors: ErrorContext| rsx! { "Oops, we encountered an error. Please report {errors:?} to the developer of this application" }, +/// Counter { +/// multiplier: "1234" +/// } +/// } /// } -/// }; +/// } +/// +/// #[component] +/// fn Counter(multiplier: String) -> Element { +/// // You can bubble up errors with `?` inside components +/// let multiplier_parsed = multiplier.parse::()?; +/// let mut count = use_signal(|| multiplier_parsed); +/// rsx! { +/// button { +/// // Or inside event handlers +/// onclick: move |_| { +/// let multiplier_parsed = multiplier.parse::()?; +/// *count.write() *= multiplier_parsed; +/// Ok(()) +/// }, +/// "{count}x{multiplier}" +/// } +/// } +/// } /// ``` /// /// ## Usage @@ -472,9 +746,9 @@ impl< #[allow(non_upper_case_globals, non_snake_case)] pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element { let error_boundary = use_error_boundary(); - match error_boundary.take_error() { - Some(error) => (props.handle_error.0)(error), - None => Some({ + let errors = error_boundary.errors(); + if errors.is_empty() { + std::result::Result::Ok({ static TEMPLATE: Template = Template { name: "examples/error_handle.rs:81:17:2342", roots: &[TemplateNode::Dynamic { id: 0usize }], @@ -487,6 +761,8 @@ pub fn ErrorBoundary(props: ErrorBoundaryProps) -> Element { Box::new([(props.children).into_dyn_node()]), Default::default(), ) - }), + }) + } else { + (props.handle_error.0)(error_boundary.clone()) } } diff --git a/packages/core/src/events.rs b/packages/core/src/events.rs index d3ddbea9f9..534df70c86 100644 --- a/packages/core/src/events.rs +++ b/packages/core/src/events.rs @@ -209,7 +209,7 @@ pub struct Callback { /// fn Child(onclick: EventHandler) -> Element { /// rsx!{ /// 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), /// } /// } @@ -270,10 +270,10 @@ impl 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)] -pub struct AsyncMarker(PhantomData); -impl + 'static, O> SpawnIfAsync, ()> for F { +pub struct AsyncMarker; +impl + 'static> SpawnIfAsync for F { fn spawn(self) { crate::prelude::spawn(async move { self.await; @@ -281,6 +281,34 @@ impl + 'static, O> SpawnIfAsync async { Result(()) } for the unit return type +#[doc(hidden)] +pub struct AsyncResultMarker; + +impl SpawnIfAsync for T +where + T: std::future::Future> + '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 #[doc(hidden)] pub struct MarkerWrapper(PhantomData); @@ -299,6 +327,22 @@ impl< } } +#[doc(hidden)] +pub struct UnitClosure(PhantomData); + +// Closure can be created from FnMut -> async { () } or FnMut -> Ret +impl< + Function: FnMut() -> Spawn + 'static, + Spawn: SpawnIfAsync + 'static, + Ret: 'static, + Marker, + > SuperFrom> for Callback<(), Ret> +{ + fn super_from(mut input: Function) -> Self { + Callback::new(move |()| input()) + } +} + #[test] fn closure_types_infer() { #[allow(unused)] @@ -315,6 +359,11 @@ fn closure_types_infer() { let callback: Callback = Callback::new(|value: u32| async move { 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 Callback { /// This borrows the callback using a RefCell. Recursively calling a callback will cause a panic. pub fn call(&self, arguments: Args) -> Ret { 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 mut callback = callback.borrow_mut(); callback(arguments) }; - Runtime::with(|rt| rt.scope_stack.borrow_mut().pop()); + Runtime::with(|rt| rt.pop_scope()); value } else { panic!("Callback was manually dropped") diff --git a/packages/core/src/fragment.rs b/packages/core/src/fragment.rs index 49c8a407a4..7b1681a0f1 100644 --- a/packages/core/src/fragment.rs +++ b/packages/core/src/fragment.rs @@ -33,7 +33,7 @@ pub fn Fragment(cx: FragmentProps) -> Element { } #[derive(Clone, PartialEq)] -pub struct FragmentProps(Element); +pub struct FragmentProps(pub(crate) Element); pub struct FragmentBuilder(Element); impl FragmentBuilder { @@ -66,7 +66,7 @@ impl FragmentBuilder { /// ```rust /// # use dioxus::prelude::*; /// fn app() -> Element { -/// rsx!{ +/// rsx! { /// CustomCard { /// h1 {} /// p {} @@ -87,7 +87,7 @@ impl FragmentBuilder { impl Properties for FragmentProps { type Builder = FragmentBuilder; fn builder() -> Self::Builder { - FragmentBuilder(None) + FragmentBuilder(VNode::empty()) } fn memoize(&mut self, new: &Self) -> bool { let equal = self == new; diff --git a/packages/core/src/global_context.rs b/packages/core/src/global_context.rs index 7821172b1b..c167f3cf35 100644 --- a/packages/core/src/global_context.rs +++ b/packages/core/src/global_context.rs @@ -1,5 +1,5 @@ -use crate::{runtime::Runtime, Element, ScopeId, Task}; -use futures_util::Future; +use crate::{innerlude::SuspendedFuture, runtime::Runtime, CapturedError, Element, ScopeId, Task}; +use std::future::Future; use std::sync::Arc; /// Get the current scope id @@ -13,6 +13,29 @@ pub fn vdom_is_rendering() -> bool { 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 + 'static) { + current_scope_id() + .expect("to be in a dioxus runtime") + .throw_error(error) +} + /// Consume context from the current scope pub fn try_consume_context() -> Option { Runtime::with_current_scope(|cx| cx.consume_context::()).flatten() @@ -52,8 +75,9 @@ pub fn provide_root_context(value: T) -> T { /// Suspended the current component on a specific task and then return None pub fn suspend(task: Task) -> Element { - Runtime::with_current_scope(|cx| cx.suspend(task)); - None + Err(crate::innerlude::RenderError::Suspended( + SuspendedFuture::new(task), + )) } /// 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 /// -/// ```rust +/// ```rust, no_run /// use dioxus::prelude::*; /// /// // prints a greeting on the initial render @@ -202,7 +226,7 @@ pub fn remove_future(id: Task) { /// /// # Custom Hook Example /// -/// ```rust +/// ```rust, no_run /// use dioxus::prelude::*; /// /// 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)); } -/// 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 pub fn use_hook_with_cleanup( hook: impl FnOnce() -> T, diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index 861fc8af89..abc39a0904 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -16,12 +16,14 @@ mod mutations; mod nodes; mod properties; mod reactive_context; -mod render_signal; +mod render_error; +mod root_wrapper; mod runtime; mod scheduler; mod scope_arena; mod scope_context; mod scopes; +mod suspense; mod tasks; mod virtual_dom; @@ -44,16 +46,18 @@ pub(crate) mod innerlude { pub use crate::nodes::*; pub use crate::properties::*; pub use crate::reactive_context::*; + pub use crate::render_error::*; pub use crate::runtime::{Runtime, RuntimeGuard}; pub use crate::scheduler::*; pub use crate::scopes::*; + pub use crate::suspense::*; pub use crate::tasks::*; pub use crate::virtual_dom::*; /// 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. - pub type Element = Option; + pub type Element = std::result::Result; /// A [`Component`] is a function that takes [`Properties`] and returns an [`Element`]. pub type Component

= 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, AnyValue, Attribute, AttributeValue, CapturedError, Component, ComponentFunction, DynamicNode, Element, ElementId, Event, Fragment, HasAttributes, IntoDynNode, MarkerWrapper, Mutation, - Mutations, NoOpMutations, Properties, RenderReturn, Runtime, ScopeId, ScopeState, SpawnIfAsync, - Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, VPlaceholder, - VText, VirtualDom, WriteMutations, + Mutations, NoOpMutations, Ok, Properties, RenderReturn, Result, Runtime, ScopeId, ScopeState, + SpawnIfAsync, Task, Template, TemplateAttribute, TemplateNode, VComponent, VNode, VNodeInner, + VPlaceholder, VText, VirtualDom, WriteMutations, }; /// 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, fc_to_builder, generation, has_context, needs_update, needs_update_any, parent_scope, provide_context, provide_root_context, queue_effect, remove_future, schedule_update, - schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, try_consume_context, - use_after_render, use_before_render, use_drop, use_error_boundary, use_hook, - use_hook_with_cleanup, wait_for_next_render, with_owner, AnyValue, Attribute, Callback, - Component, ComponentFunction, Element, ErrorBoundary, Event, EventHandler, Fragment, - HasAttributes, IntoAttributeValue, IntoDynNode, OptionStringFromMarker, Properties, - ReactiveContext, Runtime, RuntimeGuard, ScopeId, ScopeState, SuperFrom, SuperInto, Task, - Template, TemplateAttribute, TemplateNode, Throw, VNode, VNodeInner, VirtualDom, + schedule_update_any, spawn, spawn_forever, spawn_isomorphic, suspend, throw_error, + try_consume_context, use_after_render, use_before_render, use_drop, use_error_boundary, + use_hook, use_hook_with_cleanup, with_owner, AnyValue, Attribute, Callback, CapturedError, + Component, ComponentFunction, Context, Element, ErrorBoundary, ErrorContext, Event, + EventHandler, Fragment, HasAttributes, IntoAttributeValue, IntoDynNode, + OptionStringFromMarker, Properties, ReactiveContext, RenderError, Runtime, RuntimeGuard, + ScopeId, ScopeState, SuperFrom, SuperInto, SuspendedFuture, SuspenseBoundary, + SuspenseBoundaryProps, SuspenseContext, SuspenseExtension, Task, Template, + TemplateAttribute, TemplateNode, VNode, VNodeInner, VirtualDom, }; } diff --git a/packages/core/src/nodes.rs b/packages/core/src/nodes.rs index a66358bb57..f2f16fabfb 100644 --- a/packages/core/src/nodes.rs +++ b/packages/core/src/nodes.rs @@ -1,13 +1,13 @@ -use crate::innerlude::VProps; +use crate::innerlude::{RenderError, VProps}; use crate::{any_props::BoxedAnyProps, innerlude::ScopeState}; use crate::{arena::ElementId, Element, Event}; use crate::{ innerlude::{ElementRef, EventHandler, MountId}, properties::ComponentFunction, }; -use crate::{Properties, VirtualDom}; +use crate::{Properties, ScopeId, VirtualDom}; use core::panic; -use std::ops::Deref; +use std::ops::{Deref, DerefMut}; use std::rc::Rc; use std::vec; use std::{ @@ -21,28 +21,49 @@ pub type TemplateId = &'static str; /// The actual state of the component's most recent computation /// /// If the component returned early (e.g. `return None`), this will be Aborted(None) -pub enum RenderReturn { - /// A currently-available element - Ready(VNode), +#[derive(Debug)] +pub struct RenderReturn { + /// The node that was rendered + pub(crate) node: Element, +} - /// The component aborted rendering early. It might've thrown an error. - /// - /// In its place we've produced a placeholder to locate its spot in the dom when it recovers. - Aborted(VNode), +impl From for VNode { + fn from(val: RenderReturn) -> Self { + match val.node { + Ok(node) => node, + Err(RenderError::Aborted(e)) => e.render, + Err(RenderError::Suspended(fut)) => fut.placeholder, + } + } +} + +impl From for RenderReturn { + fn from(node: Element) -> Self { + RenderReturn { node } + } } impl Clone for RenderReturn { fn clone(&self) -> Self { - match self { - RenderReturn::Ready(node) => RenderReturn::Ready(node.clone_mounted()), - RenderReturn::Aborted(node) => RenderReturn::Aborted(node.clone_mounted()), + match &self.node { + Ok(node) => RenderReturn { + 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 { fn default() -> Self { - RenderReturn::Aborted(VNode::placeholder()) + RenderReturn { + node: Ok(VNode::placeholder()), + } } } @@ -50,8 +71,20 @@ impl Deref for RenderReturn { type Target = VNode; fn deref(&self) -> &Self::Target { - match self { - RenderReturn::Ready(node) | RenderReturn::Aborted(node) => node, + match &self.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 { fn drop(&mut self) { // FIXME: @@ -197,31 +236,7 @@ impl VNode { /// Create a template with no nodes that will be skipped over during diffing pub fn empty() -> Element { - use std::cell::OnceCell; - // We can reuse all placeholders across the same thread to save memory - thread_local! { - static EMPTY_VNODE: OnceCell> = 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(), - }) + Ok(Self::default()) } /// Create a template with a single placeholder node @@ -240,7 +255,7 @@ impl VNode { template: Cell::new(Template { name: "packages/core/nodes.rs:198:0:0", roots: &[TemplateNode::Dynamic { id: 0 }], - node_paths: &[&[]], + node_paths: &[&[0]], attr_paths: &[], }), }) @@ -275,12 +290,9 @@ impl VNode { /// /// Returns [`None`] if the root is actually a static node (Element/Text) pub fn dynamic_root(&self, idx: usize) -> Option<&DynamicNode> { - match &self.template.get().roots[idx] { - TemplateNode::Element { .. } | TemplateNode::Text { text: _ } => None, - TemplateNode::Dynamic { id } | TemplateNode::DynamicText { id } => { - Some(&self.dynamic_nodes[*id]) - } - } + self.template.get().roots[idx] + .dynamic_id() + .map(|id| &self.dynamic_nodes[id]) } /// 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. pub fn is_completely_dynamic(&self) -> bool { use TemplateNode::*; - self.roots - .iter() - .all(|root| matches!(root, Dynamic { .. } | DynamicText { .. })) + self.roots.iter().all(|root| matches!(root, Dynamic { .. })) + } + + /// 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 @@ -447,6 +464,20 @@ impl Template { 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 { + // 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. @@ -500,14 +531,6 @@ pub enum TemplateNode { /// The index of the dynamic node in the VNode's dynamic_nodes list 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 { @@ -515,7 +538,7 @@ impl TemplateNode { pub fn dynamic_id(&self) -> Option { use TemplateNode::*; match self { - Dynamic { id } | DynamicText { id } => Some(*id), + Dynamic { id } => Some(*id), _ => 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 pub(crate) render_fn: TypeId, + /// The props for this component 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 { /// Create a new [`VComponent`] variant pub fn new( @@ -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 { + 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 /// /// This is useful for rendering nodes outside of the VirtualDom, such as in SSR @@ -844,9 +896,7 @@ impl AnyValue for T { /// A trait that allows various items to be converted into a dynamic node for the rsx macro pub trait IntoDynNode { - /// Consume this item along with a scopestate and produce a DynamicNode - /// - /// You can use the bump alloactor of the scopestate to creat the dynamic node + /// Consume this item and produce a DynamicNode fn into_dyn_node(self) -> DynamicNode; } @@ -860,13 +910,11 @@ impl IntoDynNode for VNode { DynamicNode::Fragment(vec![self]) } } - impl IntoDynNode for DynamicNode { fn into_dyn_node(self) -> DynamicNode { self } } - impl IntoDynNode for Option { fn into_dyn_node(self) -> DynamicNode { match self { @@ -875,8 +923,23 @@ impl IntoDynNode for Option { } } } - 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 { fn into_dyn_node(self) -> DynamicNode { match self.as_ref() { Some(val) => val.clone().into_dyn_node(), @@ -884,7 +947,6 @@ impl IntoDynNode for &Element { } } } - impl IntoDynNode for &str { fn into_dyn_node(self) -> DynamicNode { DynamicNode::Text(VText { @@ -892,13 +954,11 @@ impl IntoDynNode for &str { }) } } - impl IntoDynNode for String { fn into_dyn_node(self) -> DynamicNode { DynamicNode::Text(VText { value: self }) } } - impl IntoDynNode for Arguments<'_> { fn into_dyn_node(self) -> DynamicNode { DynamicNode::Text(VText { @@ -906,7 +966,6 @@ impl IntoDynNode for Arguments<'_> { }) } } - impl IntoDynNode for &VNode { fn into_dyn_node(self) -> DynamicNode { DynamicNode::Fragment(vec![self.clone()]) @@ -929,12 +988,36 @@ impl IntoVNode for &VNode { impl IntoVNode for Element { fn into_vnode(self) -> VNode { match self { - Some(val) => val.into_vnode(), + Ok(val) => val.into_vnode(), _ => VNode::empty().unwrap(), } } } impl IntoVNode for &Element { + fn into_vnode(self) -> VNode { + match self { + Ok(val) => val.into_vnode(), + _ => VNode::empty().unwrap(), + } + } +} +impl IntoVNode for Option { + fn into_vnode(self) -> VNode { + match self { + Some(val) => val.into_vnode(), + _ => VNode::empty().unwrap(), + } + } +} +impl IntoVNode for &Option { + fn into_vnode(self) -> VNode { + match self.as_ref() { + Some(val) => val.clone().into_vnode(), + _ => VNode::empty().unwrap(), + } + } +} +impl IntoVNode for Option { fn into_vnode(self) -> VNode { match self { Some(val) => val.into_vnode(), @@ -942,6 +1025,14 @@ impl IntoVNode for &Element { } } } +impl IntoVNode for &Option { + 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. pub struct FromNodeIterator; diff --git a/packages/core/src/render_error.rs b/packages/core/src/render_error.rs new file mode 100644 index 0000000000..d4144cdf8e --- /dev/null +++ b/packages/core/src/render_error.rs @@ -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 From for RenderError { + fn from(e: E) -> Self { + Self::Aborted(CapturedError::from(e)) + } +} + +impl From for RenderError { + fn from(e: CapturedError) -> Self { + RenderError::Aborted(e) + } +} diff --git a/packages/core/src/render_signal.rs b/packages/core/src/render_signal.rs deleted file mode 100644 index 36386960aa..0000000000 --- a/packages/core/src/render_signal.rs +++ /dev/null @@ -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>>>>, -} - -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, -} - -pub(crate) struct RenderSignalFuture { - inner: Rc>, -} - -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 - } - } -} diff --git a/packages/core/src/root_wrapper.rs b/packages/core/src/root_wrapper.rs new file mode 100644 index 0000000000..91b7dea706 --- /dev/null +++ b/packages/core/src/root_wrapper.rs @@ -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) -> 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([]), + )) +} diff --git a/packages/core/src/runtime.rs b/packages/core/src/runtime.rs index 95fe448550..be8e9bc2c4 100644 --- a/packages/core/src/runtime.rs +++ b/packages/core/src/runtime.rs @@ -1,13 +1,12 @@ -use slotmap::DefaultKey; - -use crate::innerlude::Effect; +use crate::innerlude::{DirtyTasks, Effect}; +use crate::scope_context::SuspenseLocation; use crate::{ innerlude::{LocalTask, SchedulerMsg}, - render_signal::RenderSignal, scope_context::Scope, scopes::ScopeId, Task, }; +use slotmap::DefaultKey; use std::collections::BTreeSet; use std::{ cell::{Cell, Ref, RefCell}, @@ -25,6 +24,9 @@ pub struct Runtime { // We use this to track the current scope pub(crate) scope_stack: RefCell>, + // 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>, + // We use this to track the current task pub(crate) current_task: Cell>, @@ -38,25 +40,26 @@ pub struct Runtime { pub(crate) sender: futures_channel::mpsc::UnboundedSender, - // 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 pub(crate) pending_effects: RefCell>, + + // Tasks that are waiting to be polled + pub(crate) dirty_tasks: RefCell>, } impl Runtime { pub(crate) fn new(sender: futures_channel::mpsc::UnboundedSender) -> Rc { Rc::new(Self { sender, - render_signal: RenderSignal::default(), rendering: Cell::new(true), scope_states: Default::default(), scope_stack: Default::default(), + suspense_stack: Default::default(), current_task: Default::default(), tasks: Default::default(), suspended_tasks: Default::default(), pending_effects: Default::default(), + dirty_tasks: Default::default(), }) } @@ -111,15 +114,34 @@ impl Runtime { /// Useful in a limited number of scenarios pub fn on_scope(&self, id: ScopeId, f: impl FnOnce() -> O) -> O { { - self.scope_stack.borrow_mut().push(id); + self.push_scope(id); } let o = f(); { - self.scope_stack.borrow_mut().pop(); + self.pop_scope(); } 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 /// /// 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) .expect("Scheduler should exist"); } + } - // And send the render signal - self.render_signal.send(); + /// Check if we should render a scope + 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()) } } diff --git a/packages/core/src/scheduler.rs b/packages/core/src/scheduler.rs index a1a76f946f..2a87586dce 100644 --- a/packages/core/src/scheduler.rs +++ b/packages/core/src/scheduler.rs @@ -124,12 +124,13 @@ impl Hash for ScopeOrder { impl VirtualDom { /// Queue a task to be polled 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), None => { let scope = DirtyTasks::from(order); scope.queue_task(task); - self.dirty_tasks.insert(scope); + dirty_tasks.insert(scope); } } } @@ -144,15 +145,23 @@ impl VirtualDom { !self.dirty_scopes.is_empty() } - /// Take any tasks from the highest scope - pub(crate) fn pop_task(&mut self) -> Option { - let mut task = self.dirty_tasks.pop_first()?; + /// Take the top task from the highest scope + pub(crate) fn pop_task(&mut self) -> Option { + 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 - while !self.scopes.contains(task.order.id.0) { - task = self.dirty_tasks.pop_first()?; + while !self.scopes.contains(tasks.order.id.0) { + 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) } @@ -182,16 +191,21 @@ impl VirtualDom { } } - let mut dirty_task = self.dirty_tasks.first(); - // Pop any invalid tasks off of each dirty scope; - while let Some(task) = dirty_task { - if !self.scopes.contains(task.order.id.0) { - self.dirty_tasks.pop_first(); - dirty_task = self.dirty_tasks.first(); - } else { - break; + // Find the height of the highest dirty scope + let dirty_task = { + let mut dirty_tasks = self.runtime.dirty_tasks.borrow_mut(); + let mut dirty_task = dirty_tasks.first(); + // Pop any invalid tasks off of each dirty scope; + while let Some(task) = dirty_task { + if task.tasks_queued.borrow().is_empty() || !self.scopes.contains(task.order.id.0) { + dirty_tasks.pop_first(); + dirty_task = dirty_tasks.first() + } else { + break; + } } - } + dirty_task.map(|task| task.order) + }; match (dirty_scope, dirty_task) { (Some(scope), Some(task)) => { @@ -199,57 +213,27 @@ impl VirtualDom { match scope.cmp(tasks_order) { std::cmp::Ordering::Less => { let scope = self.dirty_scopes.pop_first().unwrap(); - Some(Work { - scope, - rerun_scope: true, - tasks: Default::default(), - }) + Some(Work::RerunScope(scope)) } - std::cmp::Ordering::Greater => { - let task = self.dirty_tasks.pop_first().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(), - }) + std::cmp::Ordering::Equal | std::cmp::Ordering::Greater => { + Some(Work::PollTask(self.pop_task().unwrap())) } } } (Some(_), None) => { let scope = self.dirty_scopes.pop_first().unwrap(); - Some(Work { - 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(), - }) + Some(Work::RerunScope(scope)) } + (None, Some(_)) => Some(Work::PollTask(self.pop_task().unwrap())), (None, None) => None, } } } #[derive(Debug)] -pub struct Work { - pub scope: ScopeOrder, - pub rerun_scope: bool, - pub tasks: VecDeque, +pub enum Work { + RerunScope(ScopeOrder), + PollTask(Task), } #[derive(Debug, Clone, Eq)] @@ -269,7 +253,16 @@ impl From for DirtyTasks { impl DirtyTasks { 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); } } diff --git a/packages/core/src/scope_arena.rs b/packages/core/src/scope_arena.rs index 6fe57147a4..16b5448776 100644 --- a/packages/core/src/scope_arena.rs +++ b/packages/core/src/scope_arena.rs @@ -1,26 +1,38 @@ -use crate::innerlude::ScopeOrder; -use crate::reactive_context::ReactiveContext; +use crate::innerlude::{throw_error, RenderError, RenderReturn, ScopeOrder}; +use crate::prelude::ReactiveContext; +use crate::scope_context::SuspenseLocation; use crate::{ any_props::{AnyProps, BoxedAnyProps}, innerlude::ScopeState, - nodes::RenderReturn, scope_context::Scope, scopes::ScopeId, virtual_dom::VirtualDom, }; +use crate::{Element, VNode}; 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 height = parent_id - .and_then(|parent_id| self.runtime.get_state(parent_id).map(|f| f.height + 1)) - .unwrap_or(0); + let height = match parent_id.and_then(|id| self.runtime.get_state(id)) { + Some(parent) => parent.height() + 1, + None => 0, + }; + let suspense_boundary = self + .runtime + .suspense_stack + .borrow() + .last() + .cloned() + .unwrap_or(SuspenseLocation::NotSuspended); let entry = self.scopes.vacant_entry(); 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); - self.runtime.create_scope(scope_runtime); let scope = entry.insert(ScopeState { runtime: self.runtime.clone(), @@ -30,60 +42,104 @@ impl VirtualDom { reactive_context, }); + self.runtime.create_scope(scope_runtime); + tracing::trace!("created scope {id:?} with parent {parent_id:?}"); + 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 { debug_assert!( crate::Runtime::current().is_some(), "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 new_nodes = { - let context = scope.state(); + let output = { + let scope_state = scope.state(); - context.hook_index.set(0); + scope_state.hook_index.set(0); // 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(); } - // safety: due to how we traverse the tree, we know that the scope is not currently aliased let props: &dyn AnyProps = &*scope.props; 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 - 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(); } - // And move the render generation forward by one - context.render_count.set(context.render_count.get() + 1); - // remove this scope from dirty scopes self.dirty_scopes - .remove(&ScopeOrder::new(context.height, scope_id)); - - if let Some(task) = context.last_suspendable_task.take() { - if matches!(new_nodes, RenderReturn::Aborted(_)) { - tracing::trace!("Suspending {:?} on {:?}", scope_id, task); - self.runtime.tasks.borrow().get(task.0).unwrap().suspend(); - self.runtime - .suspended_tasks - .set(self.runtime.suspended_tasks.get() + 1); - } - } + .remove(&ScopeOrder::new(scope_state.height, scope_id)); - self.runtime.scope_stack.borrow_mut().pop(); + self.runtime.pop_scope(); - new_nodes + output + } + + /// Insert any errors, or suspended tasks from an element return into the runtime + 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); + } + } } } diff --git a/packages/core/src/scope_context.rs b/packages/core/src/scope_context.rs index 3f9134d625..af0215b28c 100644 --- a/packages/core/src/scope_context.rs +++ b/packages/core/src/scope_context.rs @@ -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 rustc_hash::FxHashSet; use std::{ @@ -8,6 +12,32 @@ use std::{ 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>, + }, +} + +#[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. /// /// 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, pub(crate) shared_contexts: RefCell>>, pub(crate) spawned_tasks: RefCell>, - /// 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>, pub(crate) before_render: RefCell>>, pub(crate) after_render: RefCell>>, + + /// The suspense boundary that this scope is currently in (if any) + suspense_boundary: SuspenseLocation, + + pub(crate) status: RefCell, } impl Scope { @@ -35,6 +68,7 @@ impl Scope { id: ScopeId, parent_id: Option, height: u32, + suspense_boundary: SuspenseLocation, ) -> Self { Self { name, @@ -44,11 +78,14 @@ impl Scope { render_count: Cell::new(0), shared_contexts: RefCell::new(vec![]), spawned_tasks: RefCell::new(FxHashSet::default()), - last_suspendable_task: Cell::new(None), hooks: RefCell::new(vec![]), hook_index: Cell::new(0), before_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() } + /// 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. pub fn needs_update(&self) { self.needs_update_any(self.id) @@ -129,18 +190,18 @@ impl Scope { let mut search_parent = self.parent_id; let cur_runtime = Runtime::with(|runtime| { 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!( "looking for context {} ({:?}) in {}", std::any::type_name::(), std::any::TypeId::of::(), parent.name ); - if let Some(shared) = parent.shared_contexts.borrow().iter().find_map(|any| { - tracing::trace!("found context {:?}", (**any).type_id()); - any.downcast_ref::() - }) { - return Some(shared.clone()); + if let Some(shared) = parent.has_context() { + return Some(shared); } search_parent = parent.parent_id; } @@ -286,12 +347,6 @@ impl Scope { 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 { - self.last_suspendable_task.set(Some(task)); - None - } - /// 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). @@ -413,14 +468,6 @@ impl ScopeId { .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 { - Runtime::with_scope(self, |cx| { - cx.suspend(task); - }); - None - } - /// Pushes the future onto the poll queue to be polled after the component renders. pub fn push_future(self, fut: impl Future + 'static) -> Option { Runtime::with_scope(self, |cx| cx.spawn(fut)) @@ -466,4 +513,25 @@ impl ScopeId { .expect("to be in a dioxus runtime") .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 + 'static) { + throw_into(error, self) + } } diff --git a/packages/core/src/scopes.rs b/packages/core/src/scopes.rs index c9e0592ab4..c1e8c3c0f5 100644 --- a/packages/core/src/scopes.rs +++ b/packages/core/src/scopes.rs @@ -1,6 +1,6 @@ use crate::{ any_props::BoxedAnyProps, nodes::RenderReturn, reactive_context::ReactiveContext, - runtime::Runtime, scope_context::Scope, + scope_context::Scope, Runtime, VNode, }; use std::{cell::Ref, rc::Rc}; @@ -32,7 +32,7 @@ impl std::fmt::Debug for 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 /// that is created dynamically somewhere down the component tree. @@ -41,9 +41,24 @@ impl ScopeId { /// /// ```rust, no_run /// 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(crate) const PLACEHOLDER: ScopeId = ScopeId(usize::MAX); + + pub(crate) fn is_placeholder(&self) -> bool { + *self == Self::PLACEHOLDER + } } /// A component's rendered state. @@ -52,6 +67,8 @@ impl ScopeId { pub struct ScopeState { pub(crate) runtime: Rc, 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, pub(crate) props: BoxedAnyProps, 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. /// /// 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() .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. /// /// Returns [`None`] if the tree has not been built yet. - pub fn try_root_node(&self) -> Option<&RenderReturn> { - self.last_rendered_node.as_ref() + pub fn try_root_node(&self) -> Option<&VNode> { + 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> { diff --git a/packages/core/src/suspense/component.rs b/packages/core/src/suspense/component.rs new file mode 100644 index 0000000000..acd352f25b --- /dev/null +++ b/packages/core/src/suspense/component.rs @@ -0,0 +1,607 @@ +use crate::innerlude::*; + +/// Properties for the [`SuspenseBoundary()`] component. +#[allow(non_camel_case_types)] +pub struct SuspenseBoundaryProps { + fallback: Callback, + /// The children of the suspense boundary + children: Element, + /// THe nodes that are suspended under this boundary + pub suspended_nodes: Option, +} + +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 { + 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 { + fn into_value T>(self, default: F) -> T; +} +impl SuspenseBoundaryPropsBuilder_Optional for () { + fn into_value T>(self, default: F) -> T { + default() + } +} +impl SuspenseBoundaryPropsBuilder_Optional for (T,) { + fn into_value 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, __Marker>, + ) -> SuspenseBoundaryPropsBuilder<((Callback,), __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,), __children)> { + #[deprecated(note = "Repeated field fallback")] + #[allow(clippy::type_complexity)] + pub fn fallback( + self, + _: SuspenseBoundaryPropsBuilder_Error_Repeated_field_fallback, + ) -> SuspenseBoundaryPropsBuilder<((Callback,), __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( + self, + render_fn: impl ComponentFunction, + 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> + SuspenseBoundaryPropsBuilder<((Callback,), __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( + mount: MountId, + idx: usize, + component: &VComponent, + parent: Option, + 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( + 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( + 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( + &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) + } + } +} diff --git a/packages/core/src/suspense/mod.rs b/packages/core/src/suspense/mod.rs new file mode 100644 index 0000000000..c013de0756 --- /dev/null +++ b/packages/core/src/suspense/mod.rs @@ -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 { + 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, +} + +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 { + 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>, + id: Cell, +} + +/// Provides context methods to [`Result`] to show loading indicators for suspended results +/// +/// This trait is sealed and cannot be implemented outside of dioxus-core +pub trait SuspenseExtension: private::Sealed { + /// Add a loading indicator if the result is suspended + fn with_loading_placeholder( + self, + display_placeholder: impl FnOnce() -> Element, + ) -> std::result::Result; +} + +impl SuspenseExtension for std::result::Result { + fn with_loading_placeholder( + self, + display_placeholder: impl FnOnce() -> Element, + ) -> std::result::Result { + 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 Sealed for std::result::Result {} +} diff --git a/packages/core/src/tasks.rs b/packages/core/src/tasks.rs index d81c309b5c..e6d0ffba67 100644 --- a/packages/core/src/tasks.rs +++ b/packages/core/src/tasks.rs @@ -1,9 +1,12 @@ use crate::innerlude::Effect; use crate::innerlude::ScopeOrder; use crate::innerlude::{remove_future, spawn, Runtime}; +use crate::scope_context::ScopeStatus; +use crate::scope_context::SuspenseLocation; use crate::ScopeId; use futures_util::task::ArcWake; use slotmap::DefaultKey; +use std::marker::PhantomData; use std::sync::Arc; use std::task::Waker; 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 #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[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 { + /// 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. /// /// 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. pub fn paused(&self) -> bool { 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() } else { false @@ -62,7 +77,11 @@ impl Task { /// Wake the task. 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. @@ -73,10 +92,12 @@ impl Task { /// Set the task as active or paused. pub fn set_active(&self, active: bool) { 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); 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 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; tasks.insert_with_key(|key| { - task_id = Task(key); + task_id = Task::from_id(key); let new_task = Rc::new(LocalTask { scope, @@ -151,10 +172,10 @@ impl Runtime { parent: self.current_task(), task: RefCell::new(Box::pin(task)), waker: futures_util::task::waker(Arc::new(LocalTaskHandle { - id: task_id, + id: task_id.id, tx: self.sender.clone(), })), - ty: Cell::new(ty), + ty: RefCell::new(ty), }); local_task = Some(new_task.clone()); @@ -170,7 +191,7 @@ impl Runtime { debug_assert!(task.task.try_borrow_mut().is_ok()); self.sender - .unbounded_send(SchedulerMsg::TaskNotified(task_id)) + .unbounded_send(SchedulerMsg::TaskNotified(task_id.id)) .expect("Scheduler should exist"); task_id @@ -178,11 +199,32 @@ impl Runtime { /// Queue an effect to run after the next render pub(crate) fn queue_effect(&self, id: ScopeId, f: impl FnOnce() + 'static) { + let effect = Box::new(f) as Box; + 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, + ) { // 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 scope_order = ScopeOrder::new(id.height(), id); match effects.get(&scope_order) { - Some(effects) => effects.push_back(Box::new(f)), + Some(effects) => effects.push_back(f), None => { effects.insert(Effect::new(scope_order, f)); } @@ -196,17 +238,17 @@ impl Runtime { /// Get the parent task of the given task, if it exists pub fn parent_task(&self, task: Task) -> Option { - self.tasks.borrow().get(task.0)?.parent + self.tasks.borrow().get(task.id)?.parent } pub(crate) fn task_scope(&self, task: Task) -> Option { - 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<()> { 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 let Some(task) = task else { @@ -221,7 +263,7 @@ impl Runtime { let mut cx = std::task::Context::from_waker(&task.waker); // update the scope stack - self.scope_stack.borrow_mut().push(task.scope); + self.push_scope(task.scope); self.rendering.set(false); self.current_task.set(Some(id)); @@ -239,7 +281,7 @@ impl Runtime { } // Remove the scope from the stack - self.scope_stack.borrow_mut().pop(); + self.pop_scope(); self.rendering.set(true); 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 pub(crate) fn remove_task(&self, id: Task) -> Option> { - 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 task.suspended() { + // Remove the task from suspense + if let TaskType::Suspended { boundary } = &*task.ty.borrow() { 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 } /// Check if a task should be run during suspense pub(crate) fn task_runs_during_suspense(&self, task: Task) -> bool { let borrow = self.tasks.borrow(); - let task: Option<&LocalTask> = borrow.get(task.0).map(|t| &**t); - matches!(task, Some(LocalTask { ty, .. }) if ty.get().runs_during_suspense()) + let task: Option<&LocalTask> = borrow.get(task.id).map(|t| &**t); + matches!(task, Some(LocalTask { ty, .. }) if ty.borrow().runs_during_suspense()) } } @@ -273,49 +330,49 @@ pub(crate) struct LocalTask { parent: Option, task: RefCell + 'static>>>, waker: Waker, - ty: Cell, + ty: RefCell, active: Cell, } impl LocalTask { - pub(crate) fn suspend(&self) { - self.ty.set(TaskType::Suspended); - } - - pub(crate) fn suspended(&self) -> bool { - matches!(self.ty.get(), TaskType::Suspended) + /// Suspend the task, returns true if the task was already 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 }); + matches!(old_type, TaskType::Suspended { .. }) } } -#[derive(Clone, Copy)] +#[derive(Clone)] enum TaskType { ClientOnly, - Suspended, + Suspended { boundary: SuspenseLocation }, Isomorphic, } impl TaskType { - fn runs_during_suspense(self) -> bool { - matches!(self, TaskType::Isomorphic | TaskType::Suspended) + fn runs_during_suspense(&self) -> bool { + matches!(self, TaskType::Isomorphic | TaskType::Suspended { .. }) } } /// The type of message that can be sent to the scheduler. /// /// These messages control how the scheduler will process updates to the UI. +#[derive(Debug)] pub(crate) enum SchedulerMsg { /// Immediate updates from Components that mark them as dirty Immediate(ScopeId), /// 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 EffectQueued, } struct LocalTaskHandle { - id: Task, + id: slotmap::DefaultKey, tx: futures_channel::mpsc::UnboundedSender, } diff --git a/packages/core/src/virtual_dom.rs b/packages/core/src/virtual_dom.rs index e2ef2ff4ea..c3aeb41073 100644 --- a/packages/core/src/virtual_dom.rs +++ b/packages/core/src/virtual_dom.rs @@ -2,22 +2,22 @@ //! //! 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::{ - any_props::AnyProps, arena::ElementId, innerlude::{ - DirtyTasks, ElementRef, ErrorBoundary, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, - VNodeMount, VProps, WriteMutations, + ElementRef, NoOpMutations, SchedulerMsg, ScopeOrder, ScopeState, VNodeMount, VProps, + WriteMutations, }, - nodes::RenderReturn, nodes::{Template, TemplateId}, runtime::{Runtime, RuntimeGuard}, scopes::ScopeId, - AttributeValue, ComponentFunction, Element, Event, Mutations, VNode, + AttributeValue, ComponentFunction, Element, Event, Mutations, }; +use crate::{Task, VComponent}; use futures_util::StreamExt; -use rustc_hash::FxHashMap; use slab::Slab; use std::collections::BTreeSet; use std::{any::Any, rc::Rc}; @@ -206,18 +206,25 @@ pub struct VirtualDom { pub(crate) scopes: Slab, pub(crate) dirty_scopes: BTreeSet, - pub(crate) dirty_tasks: BTreeSet, // Maps a template path to a map of byte indexes to templates - pub(crate) templates: FxHashMap>, + // if hot reload is enabled, we need to keep track of template overrides + #[cfg(debug_assertions)] + pub(crate) templates: rustc_hash::FxHashMap>, + // Otherwise, we just need to keep track of what templates we have registered + #[cfg(not(debug_assertions))] + pub(crate) templates: rustc_hash::FxHashSet, // Templates changes that are queued for the next render pub(crate) queued_templates: Vec