diff --git a/.cargo/config.toml b/.cargo/config.toml index 74b158fbdc..6582bd95fb 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -9,7 +9,7 @@ target-dir = "rust/target" runner = 'wasm-bindgen-test-runner' rustflags = [ "--cfg=web_sys_unstable_apis", - "-Ctarget-feature=+bulk-memory,+simd128,+relaxed-simd", + "-Ctarget-feature=+bulk-memory,+simd128,+relaxed-simd,+reference-types", ] [target.i686-pc-windows-msvc] diff --git a/.github/actions/install-deps/action.yaml b/.github/actions/install-deps/action.yaml index 7eb4704cd9..a016508e51 100644 --- a/.github/actions/install-deps/action.yaml +++ b/.github/actions/install-deps/action.yaml @@ -140,7 +140,7 @@ runs: uses: dtolnay/rust-toolchain@nightly if: ${{ inputs.rust == 'true' && inputs.arch != 'aarch64' }} with: - toolchain: nightly-2024-08-29 + toolchain: nightly-2025-02-01 targets: wasm32-unknown-unknown components: rustfmt, clippy, rust-src @@ -148,7 +148,7 @@ runs: uses: dtolnay/rust-toolchain@nightly if: ${{ inputs.rust == 'true' && inputs.arch == 'aarch64' && runner.os == 'macOS' }} with: - toolchain: nightly-2024-08-29 + toolchain: nightly-2025-02-01 targets: aarch64-apple-darwin components: rustfmt, clippy, rust-src @@ -156,7 +156,7 @@ runs: uses: dtolnay/rust-toolchain@nightly if: ${{ inputs.rust == 'true' && inputs.arch == 'aarch64' && runner.os == 'Linux' }} with: - toolchain: nightly-2024-08-29 + toolchain: nightly-2025-02-01 targets: aarch64-unknown-linux-gnu components: rustfmt, clippy, rust-src diff --git a/Cargo.lock b/Cargo.lock index c13d88deb3..d32146aa1c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -17,6 +17,18 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -126,9 +138,9 @@ version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -441,9 +453,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -511,6 +523,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + [[package]] name = "crypto-common" version = "0.1.6" @@ -542,10 +579,10 @@ dependencies = [ "cc", "codespan-reporting", "once_cell", - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "scratch", - "syn 2.0.66", + "syn 2.0.99", ] [[package]] @@ -560,9 +597,9 @@ version = "1.0.122" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "928bc249a7e3cd554fd2e8e08a426e9670c50bbfc9a621653cfa9accc9641783" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -583,10 +620,10 @@ checksum = "622687fe0bac72a04e5599029151f5796111b90f1baaa9b544d807a5e31cd120" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "strsim 0.11.1", - "syn 2.0.66", + "syn 2.0.99", ] [[package]] @@ -596,8 +633,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "733cabb43482b1a1b53eee8583c2b9e8684d592215ea83efd305dd31bc2f0178" dependencies = [ "darling_core", - "quote 1.0.36", - "syn 2.0.66", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -622,8 +659,8 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "syn 1.0.109", ] @@ -732,9 +769,9 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "311a6d2f1f9d60bff73d2c78a0af97ed27f79672f15c238192a5bbb64db56d00" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -849,9 +886,9 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -939,9 +976,9 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53010ccb100b96a67bc32c0175f0ed1426b31b655d562898e57325f81c023ac0" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -1292,9 +1329,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "956caa58d4857bc9941749d55e4bd3000032d8212762586fa5705632967140e7" dependencies = [ "proc-macro-crate", - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -1327,6 +1364,10 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "serde", +] [[package]] name = "heck" @@ -1517,6 +1558,9 @@ name = "id-arena" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +dependencies = [ + "rayon", +] [[package]] name = "ident_case" @@ -1550,8 +1594,8 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9311685eb9a34808bbb0608ad2fcab9ae216266beca5848613e95553ac914e3b" dependencies = [ - "quote 1.0.36", - "syn 2.0.66", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -1842,8 +1886,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" dependencies = [ "cfg-if", - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "syn 1.0.109", ] @@ -2032,7 +2076,6 @@ dependencies = [ "serde_json", "thiserror", "tracing", - "tracing-unwrap", "ts-rs", ] @@ -2044,6 +2087,7 @@ dependencies = [ "base64 0.13.1", "chrono", "console_error_panic_hook", + "derivative", "extend", "futures", "getrandom", @@ -2185,9 +2229,9 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -2273,8 +2317,8 @@ version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e" dependencies = [ - "proc-macro2 1.0.83", - "syn 2.0.66", + "proc-macro2 1.0.94", + "syn 2.0.99", ] [[package]] @@ -2294,8 +2338,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "syn 1.0.109", "version_check", ] @@ -2306,8 +2350,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "version_check", ] @@ -2322,9 +2366,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.83" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43" +checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" dependencies = [ "unicode-ident", ] @@ -2390,7 +2434,7 @@ dependencies = [ "prost", "prost-types", "regex", - "syn 2.0.66", + "syn 2.0.99", "tempfile", ] @@ -2402,9 +2446,9 @@ checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" dependencies = [ "anyhow", "itertools 0.12.1", - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -2464,9 +2508,9 @@ version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2df2884957d2476731f987673befac5d521dff10abb0a7cbe12015bc7702fe9" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -2505,10 +2549,10 @@ version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7" dependencies = [ - "proc-macro2 1.0.83", + "proc-macro2 1.0.94", "pyo3-macros-backend", - "quote 1.0.36", - "syn 2.0.66", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -2518,10 +2562,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.83", + "proc-macro2 1.0.94", "pyo3-build-config 0.23.4", - "quote 1.0.36", - "syn 2.0.66", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -2554,11 +2598,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "c1f1914ce909e1658d9907913b4b91947430c7d9be598b15a1912935b8c04801" dependencies = [ - "proc-macro2 1.0.83", + "proc-macro2 1.0.94", ] [[package]] @@ -2591,6 +2635,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redox_syscall" version = "0.5.1" @@ -2820,9 +2884,9 @@ version = "1.0.202" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -2892,9 +2956,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" dependencies = [ "darling", - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -3031,8 +3095,8 @@ checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" dependencies = [ "heck 0.3.3", "proc-macro-error", - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "syn 1.0.109", ] @@ -3061,8 +3125,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "rustversion", "syn 1.0.109", ] @@ -3074,10 +3138,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "rustversion", - "syn 2.0.66", + "syn 2.0.99", ] [[package]] @@ -3097,19 +3161,19 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.66" +version = "2.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5" +checksum = "e02e925281e18ffd9d640e234264753c43edc62d64b2d4cf898f1bc5e75f3fc2" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "unicode-ident", ] @@ -3185,9 +3249,9 @@ version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -3270,9 +3334,9 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -3416,9 +3480,9 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -3460,15 +3524,6 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "tracing-unwrap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4e33415be97f5ae70322d6fefc696bbc08887d8835400d6c77f059469b30354" -dependencies = [ - "tracing", -] - [[package]] name = "ts-rs" version = "10.0.0" @@ -3487,9 +3542,9 @@ version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", "termcolor", ] @@ -3621,15 +3676,16 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walrus" -version = "0.20.3" +version = "0.23.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c03529cd0c4400a2449f640d2f27cd1b48c3065226d15e26d98e4429ab0adb7" +checksum = "6481311b98508f4bc2d0abbfa5d42172e7a54b4b24d8f15e28b0dc650be0c59f" dependencies = [ "anyhow", "gimli 0.26.2", "id-arena", "leb128", "log", + "rayon", "walrus-macro", "wasm-encoder", "wasmparser", @@ -3637,14 +3693,14 @@ dependencies = [ [[package]] name = "walrus-macro" -version = "0.19.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e5bd22c71e77d60140b0bd5be56155a37e5bd14e24f5f87298040d0cc40d7" +checksum = "439ad39ff894c43c9649fa724cdde9a6fc50b855d517ef071a93e5df82fe51d3" dependencies = [ - "heck 0.3.3", - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 1.0.109", + "heck 0.5.0", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] @@ -3655,11 +3711,13 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" dependencies = [ "cfg-if", + "once_cell", + "rustversion", "serde", "serde_json", "wasm-bindgen-macro", @@ -3667,32 +3725,31 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" dependencies = [ "bumpalo", "log", - "once_cell", - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-cli-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca821da8c1ae6c87c5e94493939a206daa8587caff227c6032e0061a3d80817f" +checksum = "21e1a4a49abe9cd6f762fc65fac2ef5732afeeb66be369d2f71a85b165a533cf" dependencies = [ "anyhow", - "base64 0.21.7", + "base64 0.22.1", "log", "rustc-demangle", + "serde", "serde_json", "tempfile", - "unicode-ident", "walrus", "wasm-bindgen-externref-xform", "wasm-bindgen-multi-value-xform", @@ -3704,12 +3761,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-externref-xform" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "102582726b35a30d53157fbf8de3d0f0fed4c40c0c7951d69a034e9ef01da725" +checksum = "940542c5cdbe96c35f98b5da5c65fb9d18df55a0cb1d81fc5ca4acc4fda4d61c" dependencies = [ "anyhow", "walrus", + "wasm-bindgen-wasm-conventions", ] [[package]] @@ -3726,42 +3784,46 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" dependencies = [ - "quote 1.0.36", + "quote 1.0.39", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-multi-value-xform" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3498e4799f43523d780ceff498f04d882a8dbc9719c28020034822e5952f32a4" +checksum = "64b5ad2e97adde0c3e4369c38e0dbaee329ad8f6cc2ee8e01d1d0b13bd8b14cf" dependencies = [ "anyhow", "walrus", + "wasm-bindgen-wasm-conventions", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] [[package]] name = "wasm-bindgen-test" @@ -3783,16 +3845,16 @@ version = "0.3.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" dependencies = [ - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] name = "wasm-bindgen-threads-xform" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d5add359b7f7d09a55299a9d29be54414264f2b8cf84f8c8fda5be9269b5dd9" +checksum = "1cbdf2d55a50f7edc9dd9aecae7a3a40e9736fda851bd8816f98a86167c8c277" dependencies = [ "anyhow", "walrus", @@ -3801,19 +3863,22 @@ dependencies = [ [[package]] name = "wasm-bindgen-wasm-conventions" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c04e3607b810e76768260db3a5f2e8beb477cb089ef8726da85c8eb9bd3b575" +checksum = "b1c24fcaa34d2d84407122cfb1d3f37c3586756cf462be18e049b49245a16c08" dependencies = [ "anyhow", + "leb128", + "log", "walrus", + "wasmparser", ] [[package]] name = "wasm-bindgen-wasm-interpreter" -version = "0.2.92" +version = "0.2.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ea966593c8243a33eb4d643254eb97a69de04e89462f46cf6b4f506aae89b3a" +checksum = "33f24921401faadd6944206f9d6837d07bbb5ff766ed51ad34528089f66550e0" dependencies = [ "anyhow", "log", @@ -3823,9 +3888,9 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.29.0" +version = "0.214.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18c41dbd92eaebf3612a39be316540b8377c871cb9bde6b064af962984912881" +checksum = "ff694f02a8d7a50b6922b197ae03883fbf18cdb2ae9fbee7b6148456f5f44041" dependencies = [ "leb128", ] @@ -3872,9 +3937,17 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.80.2" +version = "0.214.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "449167e2832691a1bff24cde28d2804e90e09586a448c8e76984792c44334a6b" +checksum = "5309c1090e3e84dad0d382f42064e9933fdaedb87e468cc239f0eabea73ddcb6" +dependencies = [ + "ahash", + "bitflags 2.5.0", + "hashbrown 0.14.5", + "indexmap 2.2.6", + "semver 1.0.23", + "serde", +] [[package]] name = "web-sys" @@ -4112,10 +4185,10 @@ dependencies = [ "codespan-reporting", "diffy", "dirs", - "proc-macro2 1.0.83", - "quote 1.0.36", + "proc-macro2 1.0.94", + "quote 1.0.39", "serde", - "syn 2.0.66", + "syn 2.0.99", ] [[package]] @@ -4128,9 +4201,29 @@ dependencies = [ "once_cell", "prettyplease", "proc-macro-error", - "proc-macro2 1.0.83", - "quote 1.0.36", - "syn 2.0.66", + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2 1.0.94", + "quote 1.0.39", + "syn 2.0.99", ] [[package]] diff --git a/cpp/perspective/CMakeLists.txt b/cpp/perspective/CMakeLists.txt index 10abb43d3a..988122b784 100644 --- a/cpp/perspective/CMakeLists.txt +++ b/cpp/perspective/CMakeLists.txt @@ -232,7 +232,7 @@ if(PSP_WASM_BUILD) -g3 \ -Wcast-align \ -Wover-aligned \ - -emit-tsd=perspective-server.d.ts \ + --emit-tsd=perspective-server.d.ts \ ") if (PSP_WASM_EXCEPTIONS) set(OPT_FLAGS "${OPT_FLAGS} -fwasm-exceptions ") diff --git a/examples/blocks/src/editable/index.html b/examples/blocks/src/editable/index.html index fdba271eff..6fad615870 100644 --- a/examples/blocks/src/editable/index.html +++ b/examples/blocks/src/editable/index.html @@ -29,7 +29,7 @@ viewer.load(table); } - viewer.restore({ plugin_config: { edit_mode: "EDIT" } }); + viewer.restore({ plugin_config: { edit_mode: "EDIT" }, settings: true }); diff --git a/examples/rust-axum/Cargo.toml b/examples/rust-axum/Cargo.toml index 16d78739bf..6401b09053 100644 --- a/examples/rust-axum/Cargo.toml +++ b/examples/rust-axum/Cargo.toml @@ -13,7 +13,7 @@ [package] name = "rust-axum" version = "0.1.0" -edition = "2021" +edition = "2024" publish = false [dependencies] diff --git a/examples/rust-axum/package.json b/examples/rust-axum/package.json index 86bdb14c6e..ff0a6b4fba 100644 --- a/examples/rust-axum/package.json +++ b/examples/rust-axum/package.json @@ -4,7 +4,7 @@ "version": "3.3.4", "description": "An example of a Rust/Axum virtual Perspective server", "scripts": { - "start": "cargo run" + "start": "cargo run --target=$(rustc -vV | sed -n 's|host: ||p')" }, "keywords": [], "license": "Apache-2.0", diff --git a/examples/rust-axum/src/index.html b/examples/rust-axum/src/index.html index f2932e6978..5c6eab7117 100644 --- a/examples/rust-axum/src/index.html +++ b/examples/rust-axum/src/index.html @@ -14,8 +14,9 @@ import "/node_modules/@finos/perspective-viewer/dist/cdn/perspective-viewer.js"; import perspective from "/node_modules/@finos/perspective/dist/cdn/perspective.js"; const socket = await perspective.websocket("/ws"); - const table = await socket.open_table("my_data_source"); + const table = socket.open_table("my_data_source"); const viewer = document.getElementsByTagName("perspective-viewer")[0]; + viewer.load(table); viewer.restore({ settings: true, plugin_config: { edit_mode: "EDIT" } }); diff --git a/packages/perspective-viewer-d3fc/src/ts/legend/filter.ts b/packages/perspective-viewer-d3fc/src/ts/legend/filter.ts index 7d04f9788c..6b6b224b73 100644 --- a/packages/perspective-viewer-d3fc/src/ts/legend/filter.ts +++ b/packages/perspective-viewer-d3fc/src/ts/legend/filter.ts @@ -18,7 +18,6 @@ function refineDateData(settings: Settings) { const { crossValues } = settings; crossValues.forEach(({ type }, index) => { - console.log("Type:", type); const formatType = type === "date" || type === "datetime" ? (value: any) => new Date(value).toLocaleString() diff --git a/packages/perspective-workspace/src/themes/pro-dark.less b/packages/perspective-workspace/src/themes/pro-dark.less index 224805bfc7..4e70bb94d4 100644 --- a/packages/perspective-workspace/src/themes/pro-dark.less +++ b/packages/perspective-workspace/src/themes/pro-dark.less @@ -23,7 +23,7 @@ perspective-indicator[theme="Pro Dark"] { } perspective-workspace perspective-viewer { - --status-bar--height: 38px; + --status-bar--height: 39px; } perspective-workspace perspective-viewer[settings] { diff --git a/packages/perspective-workspace/src/themes/pro.less b/packages/perspective-workspace/src/themes/pro.less index 8e263e97c0..a938573087 100644 --- a/packages/perspective-workspace/src/themes/pro.less +++ b/packages/perspective-workspace/src/themes/pro.less @@ -35,7 +35,7 @@ perspective-workspace perspective-viewer[settings] { } perspective-workspace perspective-viewer { - --status-bar--height: 38px; + --status-bar--height: 39px; } perspective-viewer[theme="Pro Light"].workspace-master-widget { diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 7822610563..674bb7481b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -11,6 +11,6 @@ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ [toolchain] -channel = "nightly-2024-08-29" +channel = "nightly-2025-02-01" components = ["rustfmt", "clippy", "rust-src"] targets = ["wasm32-unknown-unknown", "wasm32-unknown-emscripten"] diff --git a/rust/bootstrap-runtime/Cargo.toml b/rust/bootstrap-runtime/Cargo.toml index 3d0888fcaf..137935b2fb 100644 --- a/rust/bootstrap-runtime/Cargo.toml +++ b/rust/bootstrap-runtime/Cargo.toml @@ -13,7 +13,7 @@ [package] name = "perspective-bootstrap-runtime" description = "A tiny runtime wrapper to create self-extracting wasm binaries" -edition = "2021" +edition = "2024" publish = false [lib] @@ -32,4 +32,3 @@ features = ["zlib"] [features] env_target = [] default_features = [] - diff --git a/rust/bootstrap-runtime/lib.rs b/rust/bootstrap-runtime/lib.rs index 2149aa2518..be571a18c2 100644 --- a/rust/bootstrap-runtime/lib.rs +++ b/rust/bootstrap-runtime/lib.rs @@ -28,7 +28,7 @@ //! freeing the uncompressed memory from the archive. #![no_std] -#![allow(internal_features, improper_ctypes_definitions)] +#![allow(internal_features, improper_ctypes_definitions, static_mut_refs)] #![feature(core_intrinsics, lang_items, alloc_error_handler)] extern crate alloc; @@ -53,13 +53,13 @@ static ALLOCATOR: talc::Talck #[cfg(not(test))] #[panic_handler] -#[no_mangle] +#[unsafe(no_mangle)] pub fn panic(_info: &::core::panic::PanicInfo) -> ! { ::core::intrinsics::abort(); } #[alloc_error_handler] -#[no_mangle] +#[unsafe(no_mangle)] pub extern "C" fn oom(_: ::core::alloc::Layout) -> ! { ::core::intrinsics::abort(); } @@ -74,13 +74,13 @@ const COMPRESSED_BYTES: &[u8] = include_bytes!(env!("BOOTSTRAP_TARGET")); static mut DECOMPRESSED_BYTES: Vec = Vec::new(); -#[no_mangle] +#[unsafe(no_mangle)] pub fn size() -> usize { init(); unsafe { DECOMPRESSED_BYTES.len() } } -#[no_mangle] +#[unsafe(no_mangle)] pub fn offset() -> *const u8 { unsafe { DECOMPRESSED_BYTES.as_ptr() } } diff --git a/rust/bootstrap/Cargo.toml b/rust/bootstrap/Cargo.toml index 99dd458201..3aacbe34cc 100644 --- a/rust/bootstrap/Cargo.toml +++ b/rust/bootstrap/Cargo.toml @@ -13,7 +13,7 @@ [package] name = "perspective-bootstrap" description = "A CLI utility to create self-extracting wasm binaries" -edition = "2021" +edition = "2024" publish = false [[bin]] diff --git a/rust/bundle/Cargo.toml b/rust/bundle/Cargo.toml index 9e11735a32..d1c39514dd 100644 --- a/rust/bundle/Cargo.toml +++ b/rust/bundle/Cargo.toml @@ -13,7 +13,7 @@ [package] name = "perspective-bundle" description = "A CLI utility to build and link persecptive-viewer" -edition = "2021" +edition = "2024" publish = false [[bin]] diff --git a/rust/bundle/main.rs b/rust/bundle/main.rs index 02deaa2f4d..5455f4dff0 100644 --- a/rust/bundle/main.rs +++ b/rust/bundle/main.rs @@ -71,7 +71,7 @@ fn bindgen(outdir: &Path, artifact: &str, is_release: bool) { .input_path(input) .encode_into(EncodeInto::Always) .typescript(true) - .reference_types(true) + // .reference_types(true) .out_name(&format!("{}.wasm", artifact.replace('_', "-"))) .generate(outdir) .unwrap(); diff --git a/rust/generate-metadata/Cargo.toml b/rust/generate-metadata/Cargo.toml index d31fa303f4..3a1d5f6fc7 100644 --- a/rust/generate-metadata/Cargo.toml +++ b/rust/generate-metadata/Cargo.toml @@ -13,7 +13,7 @@ [package] name = "perspective-metadata" description = "A CLI utility for generating Perspective project metadata like TypeScript definitions." -edition = "2021" +edition = "2024" publish = false [[bin]] diff --git a/rust/lint/Cargo.toml b/rust/lint/Cargo.toml index 05b5e6e043..23145c00cb 100644 --- a/rust/lint/Cargo.toml +++ b/rust/lint/Cargo.toml @@ -14,7 +14,7 @@ name = "perspective-lint" description = "A CLI utility to lint rust code" version = "3.3.4" -edition = "2021" +edition = "2024" publish = false [[bin]] diff --git a/rust/perspective-client/Cargo.toml b/rust/perspective-client/Cargo.toml index 44ed92540c..7925b9b83a 100644 --- a/rust/perspective-client/Cargo.toml +++ b/rust/perspective-client/Cargo.toml @@ -14,7 +14,7 @@ name = "perspective-client" version = "3.3.4" authors = ["Andrew Stein "] -edition = "2021" +edition = "2024" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." repository = "https://github.com/finos/perspective" license = "Apache-2.0" @@ -67,7 +67,6 @@ serde_bytes = { version = "0.11" } serde_json = { version = "1.0.107", features = ["raw_value"] } thiserror = { version = "1.0.55" } tracing = { version = ">=0.1.36" } -tracing-unwrap = "1.0.1" [dependencies.prost] version = "0.12.3" diff --git a/rust/perspective-client/build.rs b/rust/perspective-client/build.rs index 8b0ef86da2..7768d3d10d 100644 --- a/rust/perspective-client/build.rs +++ b/rust/perspective-client/build.rs @@ -41,7 +41,9 @@ fn prost_build() -> Result<()> { // that crate. When protobuf-src is disabled, builders must set PROTOC // in the environment to a protocol buffer compiler. #[cfg(feature = "protobuf-src")] - std::env::set_var("PROTOC", protobuf_src::protoc()); + unsafe { + std::env::set_var("PROTOC", protobuf_src::protoc()) + }; #[cfg(not(feature = "protobuf-src"))] if std::env::var("PROTOC").is_err() { panic!( diff --git a/rust/perspective-client/src/rust/client.rs b/rust/perspective-client/src/rust/client.rs index d6aee3eddd..9a40e8a963 100644 --- a/rust/perspective-client/src/rust/client.rs +++ b/rust/perspective-client/src/rust/client.rs @@ -16,12 +16,11 @@ use std::sync::atomic::AtomicU32; use std::sync::Arc; use async_lock::{Mutex, RwLock}; -use futures::future::BoxFuture; +use futures::future::{join_all, BoxFuture, LocalBoxFuture}; use futures::Future; use nanoid::*; use prost::Message; use serde::{Deserialize, Serialize}; -use tracing_unwrap::{OptionExt, ResultExt}; use crate::proto::request::ClientReq; use crate::proto::response::ClientResp; @@ -63,8 +62,11 @@ impl GetFeaturesResp { } type BoxFn = Box O + Send + Sync + 'static>; +type Box2Fn = Box O + Send + Sync + 'static>; type Subscriptions = Arc>>; +type OnErrorCallback = + Box2Fn, Option, BoxFuture<'static, Result<(), ClientError>>>; type OnceCallback = Box ClientResult<()> + Send + Sync + 'static>; type SendCallback = Arc< dyn for<'a> Fn(&'a Request) -> BoxFuture<'a, Result<(), Box>> @@ -86,6 +88,7 @@ pub struct Client { features: Arc>>, send: SendCallback, id_gen: Arc, + subscriptions_errors: Subscriptions, subscriptions_once: Subscriptions, subscriptions: Subscriptions>>>, } @@ -98,6 +101,15 @@ impl std::fmt::Debug for Client { } } +/// The type of the `reconnect` parameter passed to [`Client::handle_error`}, +/// and to the callback closure of [`Client::on_error`]. +/// +/// Calling this function from a [`Client::on_error`] closure should run the +/// (implementation specific) client reconnect logic, e.g. rebindign a +/// websocket. +pub type ReconnectCallback = + Arc LocalBoxFuture<'static, Result<(), Box>> + Send + Sync>; + impl Client { /// Create a new client instance with a closure that handles message /// dispatch. See [`Client::new`] for details. @@ -119,9 +131,10 @@ impl Client { Client { features: Arc::default(), id_gen: Arc::new(AtomicU32::new(1)), - subscriptions_once: Arc::default(), - subscriptions: Subscriptions::default(), send, + subscriptions: Subscriptions::default(), + subscriptions_errors: Arc::default(), + subscriptions_once: Arc::default(), } } @@ -160,6 +173,26 @@ impl Client { Ok(false) } + pub async fn handle_error( + &self, + message: Option, + reconnect: Option, + ) -> ClientResult<()> { + let subs = self.subscriptions_errors.read().await; + let tasks = join_all( + subs.values() + .map(|callback| callback(message.clone(), reconnect.clone())), + ); + tasks.await.into_iter().collect::>()?; + Ok(()) + } + + pub async fn on_error(&self, on_error: OnErrorCallback) -> ClientResult { + let id = self.gen_id(); + self.subscriptions_errors.write().await.insert(id, on_error); + Ok(id) + } + pub async fn init(&self) -> ClientResult<()> { let msg = Request { msg_id: self.gen_id(), @@ -207,7 +240,7 @@ impl Client { tracing::debug!("SEND {}", msg); if let Err(e) = (self.send)(msg).await { self.subscriptions_once.write().await.remove(&msg.msg_id); - Err(e.into()) + Err(ClientError::Unknown(e.to_string())) } else { Ok(()) } @@ -225,7 +258,7 @@ impl Client { tracing::debug!("SEND {}", msg); if let Err(e) = (self.send)(msg).await { self.subscriptions.write().await.remove(&msg.msg_id); - Err(e.into()) + Err(ClientError::Unknown(e.to_string())) } else { Ok(()) } @@ -273,7 +306,7 @@ impl Client { let table = table.clone(); move |update: crate::proto::ViewOnUpdateResp| { let table = table.clone(); - let update = update.delta.unwrap_or_log(); + let update = update.delta.expect("Missing update"); async move { table .update( diff --git a/rust/perspective-client/src/rust/lib.rs b/rust/perspective-client/src/rust/lib.rs index f3eb48c54b..b43de0aba9 100644 --- a/rust/perspective-client/src/rust/lib.rs +++ b/rust/perspective-client/src/rust/lib.rs @@ -29,7 +29,7 @@ pub mod config; mod proto; pub mod utils; -pub use crate::client::{Client, ClientHandler, Features, SystemInfo}; +pub use crate::client::{Client, ClientHandler, Features, ReconnectCallback, SystemInfo}; pub use crate::proto::{ColumnType, SortOp, ViewOnUpdateResp}; pub use crate::session::{ProxySession, Session}; pub use crate::table::{ diff --git a/rust/perspective-client/src/rust/session.rs b/rust/perspective-client/src/rust/session.rs index 15e2c1238a..d34d551b03 100644 --- a/rust/perspective-client/src/rust/session.rs +++ b/rust/perspective-client/src/rust/session.rs @@ -107,7 +107,7 @@ impl ProxySession { fn encode(response: Response, callback: ProxyCallback) -> Result<(), ClientError> { let mut enc = vec![]; response.encode(&mut enc)?; - callback(&enc)?; + callback(&enc).map_err(|x| ClientError::Unknown(x.to_string()))?; Ok(()) } diff --git a/rust/perspective-client/src/rust/utils/mod.rs b/rust/perspective-client/src/rust/utils/mod.rs index 8445bad9d1..5ceb8657a8 100644 --- a/rust/perspective-client/src/rust/utils/mod.rs +++ b/rust/perspective-client/src/rust/utils/mod.rs @@ -57,7 +57,7 @@ pub enum ClientError { #[error("Can't use both `limit` and `index` arguments")] BadTableOptions, - #[error("External error: {0:?}")] + #[error("External error: {0}")] ExternalError(#[from] Box), #[error("Undecipherable proto message")] @@ -77,3 +77,18 @@ impl From for ClientError { } } } + +pub trait PerspectiveResultExt { + fn unwrap_or_log(&self); +} + +impl PerspectiveResultExt for Result +where + E: std::error::Error, +{ + fn unwrap_or_log(&self) { + if let Err(e) = self { + tracing::warn!("{}", e); + } + } +} diff --git a/rust/perspective-js/Cargo.toml b/rust/perspective-js/Cargo.toml index cb575b4eef..90da6a65c3 100644 --- a/rust/perspective-js/Cargo.toml +++ b/rust/perspective-js/Cargo.toml @@ -14,7 +14,7 @@ name = "perspective-js" version = "3.3.4" authors = ["Andrew Stein "] -edition = "2021" +edition = "2024" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." repository = "https://github.com/finos/perspective" license = "Apache-2.0" @@ -54,6 +54,7 @@ base64 = "0.13.0" chrono = "0.4" extend = "1.1.2" futures = "0.3.28" +derivative = "2.2.0" getrandom = { version = "0.2", features = ["js"] } js-intern = "0.3.1" js-sys = "0.3.64" @@ -73,7 +74,7 @@ ts-rs = { version = "10.0.0", features = [ tracing = { version = ">=0.1.36" } tracing-subscriber = "0.3.15" console_error_panic_hook = "0.1.6" -wasm-bindgen = { version = "=0.2.92", features = ["serde-serialize"] } +wasm-bindgen = { version = "=0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" [dependencies.web-sys] diff --git a/rust/perspective-js/src/rust/client.rs b/rust/perspective-js/src/rust/client.rs index 109ad83914..5120d18a98 100644 --- a/rust/perspective-js/src/rust/client.rs +++ b/rust/perspective-js/src/rust/client.rs @@ -10,12 +10,20 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::error::Error; +use std::future::Future; +use std::sync::Arc; + +use derivative::Derivative; +use futures::channel::oneshot; +use futures::future::LocalBoxFuture; use js_sys::{Function, Uint8Array}; use macro_rules_attribute::apply; #[cfg(doc)] use perspective_client::SystemInfo; -use perspective_client::{Session, TableData, TableInitOptions}; +use perspective_client::{ReconnectCallback, Session, TableData, TableInitOptions}; use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::{future_to_promise, JsFuture}; pub use crate::table::*; use crate::utils::{inherit_docs, ApiError, ApiResult, JsValueSerdeExt, LocalPollLoop}; @@ -81,22 +89,65 @@ pub struct Client { pub(crate) client: perspective_client::Client, } +/// A wrapper around [`js_sys::Function`] to ease async integration for the +/// `reconnect` argument of [`Client::on_error`] callback. +#[derive(Derivative)] +#[derivative(Clone(bound = ""))] +struct JsReconnect(Arc js_sys::Promise>); + +unsafe impl Send for JsReconnect {} +unsafe impl Sync for JsReconnect {} + +impl JsReconnect { + fn run(&self, args: I) -> js_sys::Promise { + self.0(args) + } + + fn run_all( + &self, + args: I, + ) -> impl Future>> + Send + Sync + 'static + { + let (sender, receiver) = oneshot::channel::>>(); + let p = self.0(args); + let _ = future_to_promise(async move { + let result = JsFuture::from(p) + .await + .map(|_| ()) + .map_err(|x| format!("{:?}", x).into()); + + sender.send(result).unwrap(); + Ok(JsValue::UNDEFINED) + }); + + async move { receiver.await.unwrap() } + } +} + +impl From for JsReconnect +where + F: Fn(I) -> js_sys::Promise + 'static, +{ + fn from(value: F) -> Self { + JsReconnect(Arc::new(value)) + } +} + #[wasm_bindgen] impl Client { #[wasm_bindgen(constructor)] pub fn new(send_request: Function, close: Option) -> Self { - let send1 = send_request.clone(); - let send_loop = LocalPollLoop::new(move |mut buff: Vec| { - let buff2 = unsafe { js_sys::Uint8Array::view_mut_raw(buff.as_mut_ptr(), buff.len()) }; - send1.call1(&JsValue::UNDEFINED, &buff2) + let send_request = JsReconnect::from(move |mut v: Vec| { + let buff2 = unsafe { js_sys::Uint8Array::view_mut_raw(v.as_mut_ptr(), v.len()) }; + send_request + .call1(&JsValue::UNDEFINED, &buff2) + .unwrap() + .unchecked_into::() }); let client = perspective_client::Client::new_with_callback(move |msg| { - let task = send_loop.poll(msg.to_vec()); - Box::pin(async move { - task.await; - Ok(()) - }) + let vec = msg.to_vec(); + Box::pin(send_request.run_all(vec)) }); Client { close, client } @@ -122,6 +173,93 @@ impl Client { Ok(()) } + #[doc(hidden)] + #[wasm_bindgen] + pub async fn handle_error( + &self, + error: Option, + reconnect: Option, + ) -> ApiResult<()> { + self.client + .handle_error( + error, + reconnect.map(|reconnect| { + let reconnect = + JsReconnect::from(move |()| match reconnect.call0(&JsValue::UNDEFINED) { + Ok(x) => x.unchecked_into::(), + Err(e) => { + // This error may occur when _invoking_ the function + tracing::warn!("{:?}", e); + js_sys::Promise::reject(&format!("C {:?}", e).into()) + }, + }); + + Arc::new(move || { + let fut = JsFuture::from(reconnect.run(())); + Box::pin(async move { + // This error may occur when _awaiting_ the promise returned by the + // function + if let Err(e) = fut.await { + if let Some(e) = e.dyn_ref::() { + Err(e.to_string().as_string().unwrap().into()) + } else { + Err(e.as_string().unwrap().into()) + } + } else { + Ok(()) + } + }) + as LocalBoxFuture<'static, Result<(), Box>> + }) as Arc<(dyn Fn() -> _ + Send + Sync)> + }), + ) + .await?; + + Ok(()) + } + + #[doc(hidden)] + #[wasm_bindgen] + pub async fn on_error(&self, callback: Function) -> ApiResult { + let callback = JsReconnect::from( + move |(message, reconnect): (Option, Option)| { + let cl: Closure js_sys::Promise> = Closure::new(move || { + let reconnect = reconnect.clone(); + future_to_promise(async move { + if let Some(f) = reconnect { + f().await.map_err(|e| JsValue::from(format!("A {}", e)))?; + } + + Ok(JsValue::UNDEFINED) + }) + }); + + if let Err(e) = callback.call2( + &JsValue::UNDEFINED, + &JsValue::from(message), + &cl.into_js_value(), + ) { + tracing::warn!("D {:?}", e); + } + + js_sys::Promise::resolve(&JsValue::UNDEFINED) + }, + ); + + let id = self + .client + .on_error(Box::new(move |message, reconnect| { + let callback = callback.clone(); + Box::pin(async move { + let _promise = callback.run((message, reconnect)); + Ok(()) + }) + })) + .await?; + + Ok(id) + } + #[apply(inherit_docs)] #[inherit_doc = "client/table.md"] #[wasm_bindgen] diff --git a/rust/perspective-js/src/rust/utils/errors.rs b/rust/perspective-js/src/rust/utils/errors.rs index 4d0b6828b2..42562f5bbc 100644 --- a/rust/perspective-js/src/rust/utils/errors.rs +++ b/rust/perspective-js/src/rust/utils/errors.rs @@ -47,6 +47,18 @@ impl ApiError { } } +impl Display for ApiError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(msg) = self.0.as_string() { + write!(f, "{}", msg) + } else { + write!(f, "{:?}", self.0) + } + } +} + +impl std::error::Error for ApiError {} + /// A common Rust error handling idion (see e.g. `anyhow::Result`) pub type ApiResult = Result; @@ -75,6 +87,7 @@ macro_rules! define_api_error { } define_api_error!( + Box, serde_wasm_bindgen::Error, std::io::Error, serde_json::Error, @@ -92,11 +105,11 @@ define_api_error!( #[wasm_bindgen(inline_js = r#" export class PerspectiveViewNotFoundError extends Error {} "#)] -extern "C" { +unsafe extern "C" { pub type PerspectiveViewNotFoundError; #[wasm_bindgen(constructor)] - fn new() -> PerspectiveViewNotFoundError; + unsafe fn new() -> PerspectiveViewNotFoundError; } /// Explicit conversion methods for `ApiResult`, for situations where @@ -121,7 +134,9 @@ impl ToApiError for Result<(), ApiResult> { impl From for ApiError { fn from(value: ClientError) -> Self { match value { - ClientError::ViewNotFound => ApiError(PerspectiveViewNotFoundError::new().into()), + ClientError::ViewNotFound => { + ApiError(unsafe { PerspectiveViewNotFoundError::new() }.into()) + }, err => ApiError(JsError::new(format!("{}", err).as_str()).into()), } } diff --git a/rust/perspective-js/src/rust/utils/futures.rs b/rust/perspective-js/src/rust/utils/futures.rs index 5ee6fbd62a..d50d30a7fa 100644 --- a/rust/perspective-js/src/rust/utils/futures.rs +++ b/rust/perspective-js/src/rust/utils/futures.rs @@ -124,7 +124,7 @@ where #[inline] unsafe fn from_abi(js: Self::Abi) -> Self { Self::new(async move { - let promise = js_sys::Promise::from_abi(js); + let promise = unsafe { js_sys::Promise::from_abi(js) }; Ok(JsFuture::from(promise).await?.into()) }) } diff --git a/rust/perspective-js/src/rust/utils/local_poll_loop.rs b/rust/perspective-js/src/rust/utils/local_poll_loop.rs index 4ddbae3cea..f0a031e93e 100644 --- a/rust/perspective-js/src/rust/utils/local_poll_loop.rs +++ b/rust/perspective-js/src/rust/utils/local_poll_loop.rs @@ -17,6 +17,7 @@ use wasm_bindgen_futures::spawn_local; /// A useful abstraction for connecting `!Sync + !Send` callbacks (like /// `js_sys::Function`) to `Send + Sync` contexts (like the client loop). +#[derive(Clone)] pub struct LocalPollLoop(UnboundedSender); impl LocalPollLoop { @@ -36,8 +37,26 @@ impl LocalPollLoop { Self(emit) } + /// Create a new loop which accepts a `R: Send + Sync` intermediate state + /// argument and calls the `!Send + !Sync` callback. + pub fn new_async FUT + 'static, FUT: Future>>( + send: F, + ) -> Self { + let (emit, mut receive) = unbounded::(); + spawn_local(async move { + while let Some(resp) = receive.next().await { + let resp = send(resp).await; + if let Err(err) = resp { + web_sys::console::error_2(&"Failed to serialize".into(), &err); + } + } + }); + + Self(emit) + } + /// Send a new `R` to the poll loop. - pub fn poll(&self, msg: R) -> impl Future + Send + Sync + 'static { + pub fn poll(&self, msg: R) -> impl Future + Send + Sync + 'static + use { let mut emit = self.0.clone(); async move { emit.send(msg).await.unwrap() } } diff --git a/rust/perspective-js/src/rust/view.rs b/rust/perspective-js/src/rust/view.rs index b0c0856147..e06f9e4bf4 100644 --- a/rust/perspective-js/src/rust/view.rs +++ b/rust/perspective-js/src/rust/view.rs @@ -21,7 +21,7 @@ use crate::table::Table; use crate::utils::{inherit_docs, ApiFuture, ApiResult, JsValueSerdeExt, LocalPollLoop}; #[wasm_bindgen] -extern "C" { +unsafe extern "C" { #[wasm_bindgen(typescript_type = "ViewWindow")] #[derive(Clone)] pub type JsViewWindow; diff --git a/rust/perspective-js/src/ts/perspective.node.ts b/rust/perspective-js/src/ts/perspective.node.ts index ce5b722151..12d5f8a2f1 100644 --- a/rust/perspective-js/src/ts/perspective.node.ts +++ b/rust/perspective-js/src/ts/perspective.node.ts @@ -27,6 +27,8 @@ import { load_wasm_stage_0 } from "./wasm/decompress.js"; import { PerspectiveServer } from "./wasm/engine.ts"; import { compile_perspective } from "./wasm/emscripten_api.ts"; +import * as psp_websocket from "./websocket.ts"; + const __dirname = path.dirname(url.fileURLToPath(import.meta.url)); if (!globalThis.crypto) { @@ -68,7 +70,7 @@ export const make_session = async ( // Helper function to create client emitter/receiver pairs export function make_client( - send_request: (buffer: Uint8Array) => void, + send_request: (buffer: Uint8Array) => Promise, close?: Function ) { return new perspective_client.Client(send_request, close); @@ -166,13 +168,15 @@ function buffer_to_arraybuffer( } } -function invert_promise(): [(t: T) => void, Promise] { - let sender: ((t: T) => void) | undefined = undefined; - let receiver: Promise = new Promise((x) => { +function invert_promise(): [(t: T) => void, Promise, (t: any) => void] { + let sender: ((t: T) => void) | undefined = undefined, + reject = undefined; + let receiver: Promise = new Promise((x, u) => { sender = x; + reject = u; }); - return [sender!, receiver]; + return [sender!, receiver, reject!]; } export class WebSocketServer { @@ -272,27 +276,11 @@ export function table( export async function websocket( url: string ): Promise { - const ws = new WebSocket(url); - let [sender, receiver] = invert_promise(); - ws.onopen = sender; - ws.binaryType = "arraybuffer"; - await receiver; - const client = make_client( - (proto) => { - ws.send(proto.slice().buffer); - }, - () => { - console.log("Closing websocket"); - ws.close(); - } + return await psp_websocket.websocket( + WebSocket as unknown as typeof window.WebSocket, + perspective_client.Client, + url ); - - ws.onmessage = (msg) => { - client.handle_response(msg.data); - }; - - await client.init(); - return client; } export default { diff --git a/rust/perspective-js/src/ts/wasm/browser.ts b/rust/perspective-js/src/ts/wasm/browser.ts index 667a4e6d29..626f10854e 100644 --- a/rust/perspective-js/src/ts/wasm/browser.ts +++ b/rust/perspective-js/src/ts/wasm/browser.ts @@ -12,13 +12,24 @@ import type * as psp from "../../../dist/wasm/perspective-js.d.ts"; -function invert_promise(): [(t: T) => void, Promise] { - let sender; - let receiver: Promise = new Promise((x) => { +import * as psp_websocket from "../websocket.ts"; + +function invert_promise(): [ + (t: T) => void, + Promise, + (e: string) => void +] { + let sender, reject; + let receiver: Promise = new Promise((x, y) => { sender = x; + reject = y; }); - return [sender as unknown as (t: T) => void, receiver]; + return [ + sender as unknown as (t: T) => void, + receiver, + reject as unknown as (e: string) => void, + ]; } async function _init(ws: MessagePort | Worker, wasm: WebAssembly.Module) { @@ -69,11 +80,11 @@ export async function worker( } const client = new Client( - (proto: Uint8Array) => { + async (proto: Uint8Array) => { const f = proto.slice().buffer; port.postMessage(f, { transfer: [f] }); }, - () => { + async () => { console.debug("Closing WebWorker"); port.close(); } @@ -99,29 +110,8 @@ export async function websocket( module: Promise, url: string | URL ) { - const ws = new WebSocket(url); - let [sender, receiver] = invert_promise(); - ws.onopen = sender; - ws.binaryType = "arraybuffer"; - await receiver; const { Client } = await module; - const client = new Client( - (proto: Uint8Array) => { - const buffer = proto.slice().buffer; - ws.send(buffer); - }, - () => { - console.debug("Closing WebSocket"); - ws.close(); - } - ); - - ws.onmessage = (msg) => { - client.handle_response(msg.data); - }; - - await client.init(); - return client; + return await psp_websocket.websocket(WebSocket, Client, url); } export default { websocket, worker }; diff --git a/rust/perspective-js/src/ts/websocket.ts b/rust/perspective-js/src/ts/websocket.ts new file mode 100644 index 0000000000..0fb51ff6a7 --- /dev/null +++ b/rust/perspective-js/src/ts/websocket.ts @@ -0,0 +1,98 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import type * as perspective_client from "../../dist/wasm/perspective-js.js"; + +function invert_promise(): [(t: T) => void, Promise, (t: any) => void] { + let sender: ((t: T) => void) | undefined = undefined, + reject: ((t: any) => void) | undefined = undefined; + let receiver: Promise = new Promise((x, u) => { + sender = x; + reject = u; + }); + + return [sender!, receiver, reject!]; +} + +/** + * Create a new client connected via WebSocket to a server implemnting the + * Perspective Protocol. + * @param module + * @param url + * @returns + */ +export async function websocket( + WebSocket: typeof window.WebSocket, + Client: typeof perspective_client.Client, + url: string | URL +): Promise { + let client: perspective_client.Client, ws: WebSocket; + + async function connect() { + if ( + ws?.readyState === WebSocket.CONNECTING || + ws?.readyState === WebSocket.OPEN + ) { + console.warn(`Already connected ${ws.readyState}`); + return; + } + + let [sender, receiver, reject] = invert_promise(); + ws = new WebSocket(url); + ws.onopen = sender; + ws.binaryType = "arraybuffer"; + ws.onerror = (_event) => { + client.handle_error(`WebSocket error`, connect); + reject(`WebSocket error`); + }; + + ws.onclose = (event) => { + // https://www.rfc-editor.org/rfc/rfc6455#section-7.4.1 + const msg = `WebSocket closed ${event.code}`; + client.handle_error(msg, connect); + reject(msg); + }; + + ws.onmessage = (event) => { + client.handle_response(event.data); + }; + + await receiver; + } + + async function send_message(proto: Uint8Array) { + if ( + ws.readyState === WebSocket.CLOSING || + ws.readyState === WebSocket.CLOSED + ) { + const msg = `WebSocket transport error (${ws.readyState})`; + client.handle_error(msg, connect); + throw new Error(msg); + } else if (ws.readyState === WebSocket.CONNECTING) { + const msg = `WebSocket message dropped (${ws.readyState})`; + throw new Error(msg); + } else { + const buffer = proto.slice().buffer; + ws.send(buffer); + } + } + + async function on_close() { + console.debug("Closing WebSocket"); + ws.close(); + } + + client = new Client(send_message, on_close); + await connect(); + await client.init(); + return client; +} diff --git a/rust/perspective-js/test/js/proxy_session.spec.js b/rust/perspective-js/test/js/proxy_session.spec.js index 94d536f1e0..1b40d9c872 100644 --- a/rust/perspective-js/test/js/proxy_session.spec.js +++ b/rust/perspective-js/test/js/proxy_session.spec.js @@ -14,7 +14,6 @@ import { test, expect } from "@finos/perspective-test"; import { make_client, make_server } from "@finos/perspective"; test("Proxy session tunnels requests through client", async () => { - // test.setTimeout(2000); const { client, server } = connectClientToServer(); const { proxyClient } = connectProxyClient(client); @@ -74,12 +73,15 @@ test("Proxy session tunnels on_update callbacks through client", async () => { function connectClientToServer() { const server = make_server(); - const session = server.make_session((msg) => { - client.handle_response(msg); + const session = server.make_session(async (msg) => { + await client.handle_response(msg); }); - const client = make_client((msg) => { + + const client = make_client(async (msg) => { session.handle_request(msg); + session.poll(); }); + return { client, server, diff --git a/rust/perspective-js/test/js/remote.spec.js b/rust/perspective-js/test/js/remote.spec.js index c6107c7e7b..44ff78fab9 100644 --- a/rust/perspective-js/test/js/remote.spec.js +++ b/rust/perspective-js/test/js/remote.spec.js @@ -45,6 +45,17 @@ test.describe("WebSocketManager", function () { await table.delete(); }); + test("Throws an exception when the server is detached", async () => { + const data = [{ x: 1 }]; + const _table = await perspective.table(data, { name: "test" }); + const client = await perspective.websocket(`ws://localhost:${port}`); + const client_table = await client.open_table("test"); + const client_view = await client_table.view(); + await server.close(); + const client_data = client_view.to_json(); + await expect(client_data).rejects.toThrow(); + }); + test("passes back errors from server", async () => { expect.assertions(2); const data = [{ x: 1 }]; diff --git a/rust/perspective-python/Cargo.toml b/rust/perspective-python/Cargo.toml index ea9f612b1a..0691386ee6 100644 --- a/rust/perspective-python/Cargo.toml +++ b/rust/perspective-python/Cargo.toml @@ -13,7 +13,7 @@ [package] name = "perspective-python" version = "3.3.4" -edition = "2021" +edition = "2024" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." repository = "https://github.com/finos/perspective" license = "Apache-2.0" diff --git a/rust/perspective-python/build.rs b/rust/perspective-python/build.rs index 1677dcfcc4..d4f7987e9c 100644 --- a/rust/perspective-python/build.rs +++ b/rust/perspective-python/build.rs @@ -25,6 +25,6 @@ fn main() -> Result<(), Box> { return Ok(()); } - std::env::set_var("CARGO_FEATURE_PYTHON", "1"); + unsafe { std::env::set_var("CARGO_FEATURE_PYTHON", "1") }; Ok(()) } diff --git a/rust/perspective-python/src/client/client_async.rs b/rust/perspective-python/src/client/client_async.rs index d4a5e22dda..d891bbda49 100644 --- a/rust/perspective-python/src/client/client_async.rs +++ b/rust/perspective-python/src/client/client_async.rs @@ -534,7 +534,7 @@ impl AsyncView { } #[pyo3(signature = (**window))] - pub async fn to_records<'a>(&self, window: Option>) -> PyResult> { + pub async fn to_records(&self, window: Option>) -> PyResult> { let json = self.to_json_string(window).await?; Python::with_gil(|py| { let json_module = PyModule::import(py, "json")?; @@ -544,7 +544,7 @@ impl AsyncView { } #[pyo3(signature = (**window))] - pub async fn to_json<'a>(&self, window: Option>) -> PyResult> { + pub async fn to_json(&self, window: Option>) -> PyResult> { self.to_records(window).await } } @@ -626,7 +626,7 @@ mod py_async { #[allow(unused)] pub fn into_future( awaitable: Bound, - ) -> PyResult> + Send> { + ) -> PyResult> + Send + use<>> { pyo3_async_runtimes::generic::into_future::(awaitable) } } diff --git a/rust/perspective-server/Cargo.toml b/rust/perspective-server/Cargo.toml index e5073c6e5d..dfe57aa717 100644 --- a/rust/perspective-server/Cargo.toml +++ b/rust/perspective-server/Cargo.toml @@ -14,7 +14,7 @@ name = "perspective-server" version = "3.3.4" authors = ["Andrew Stein "] -edition = "2021" +edition = "2024" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." repository = "https://github.com/finos/perspective" license = "Apache-2.0" diff --git a/rust/perspective-server/src/ffi.rs b/rust/perspective-server/src/ffi.rs index e239a1492e..7d004b5961 100644 --- a/rust/perspective-server/src/ffi.rs +++ b/rust/perspective-server/src/ffi.rs @@ -23,7 +23,7 @@ pub struct CppResponseBatch { entries_ptr: usize, } -extern "C" { +unsafe extern "C" { fn psp_alloc(size: usize) -> *mut u8; fn psp_free(ptr: *const u8); fn psp_new_server() -> *const u8; diff --git a/rust/perspective-viewer/Cargo.toml b/rust/perspective-viewer/Cargo.toml index 04b7b75666..ca7b2b39c2 100644 --- a/rust/perspective-viewer/Cargo.toml +++ b/rust/perspective-viewer/Cargo.toml @@ -14,7 +14,7 @@ name = "perspective-viewer" version = "3.3.4" authors = ["Andrew Stein "] -edition = "2021" +edition = "2024" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." repository = "https://github.com/finos/perspective" license = "Apache-2.0" @@ -106,8 +106,9 @@ ts-rs = { version = "10.0.0", features = [ tracing = { version = ">=0.1.36" } tracing-subscriber = "0.3.15" + # Browser API bindings -wasm-bindgen = { version = "=0.2.92", features = ["serde-serialize"] } +wasm-bindgen = { version = "=0.2.100", features = ["serde-serialize"] } # Browser `Promise` bindings wasm-bindgen-futures = "0.4.41" diff --git a/rust/perspective-viewer/src/less/status-bar.less b/rust/perspective-viewer/src/less/status-bar.less index 4e93963486..8b5eb22c48 100644 --- a/rust/perspective-viewer/src/less/status-bar.less +++ b/rust/perspective-viewer/src/less/status-bar.less @@ -34,6 +34,10 @@ } } + #status_bar.updating { + box-shadow: none; + } + #status_bar { box-shadow: 0 13px 0 -12px var(--inactive--border-color); z-index: 1; @@ -155,8 +159,8 @@ // font-size: var(--label--font-size, 0.75em); margin: 0px 14px; user-select: none; - height: 100%; - line-height: 36px; + // height: 100%; + // line-height: 36px; &:before { position: relative; @@ -167,9 +171,9 @@ } } - span#rows { - margin-left: 2px; - } + // span#rows { + // margin-left: 2px; + // } span.icon { height: 100%; @@ -207,6 +211,48 @@ -webkit-mask-image: var(--status-ok-icon--mask-image); } + .error-dialog { + display: none; + } + + div#status_reconnect { + display: flex; + align-items: center; + height: var(--status-bar--height, 48px); + &.errored { + cursor: pointer; + } + + &.errored.disabled { + cursor: auto; + pointer-events: none; + } + + span#status.errored { + display: flex; + align-items: center; + justify-content: center; + height: 20px; + border-radius: 10px; + color: var(--plugin--background); + height: 20px; + // pointer-events: all; + // mask-image: url(../svg/status_error.svg); + // -webkit-mask-image: url(../svg/status_error.svg); + &:before { + content: "!"; + } + } + + &.errored:hover { + background-color: var(--icon--color); + span#status.errored { + color: var(--icon--color); + background-color: var(--plugin--background); + } + } + } + // Status bar status icon animations span#status_updating { position: absolute; diff --git a/rust/perspective-viewer/src/rust/components/column_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector.rs index 258e7a1b33..93ee7ab361 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector.rs @@ -183,8 +183,11 @@ impl Component for ColumnSelector { &ctx.props().renderer.metadata(), ); - ApiFuture::spawn(ctx.props().update_and_render(update)); + if let Ok(task) = ctx.props().update_and_render(update) { + ApiFuture::spawn(task); + } } + true }, Drop((column, DragTarget::Active, effect, index)) => { @@ -196,7 +199,10 @@ impl Component for ColumnSelector { &ctx.props().renderer.metadata(), ); - ApiFuture::spawn(ctx.props().update_and_render(update)); + if let Ok(task) = ctx.props().update_and_render(update) { + ApiFuture::spawn(task); + } + true }, Drop((_, _, DragEffect::Move(DragTarget::Active), _)) => true, diff --git a/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs index 2899b25ef1..fa14099774 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/active_column.rs @@ -12,14 +12,15 @@ use std::collections::HashSet; -use perspective_client::config::*; use perspective_client::ColumnType; +use perspective_client::config::*; +use perspective_client::utils::PerspectiveResultExt; use web_sys::*; use yew::prelude::*; +use super::InPlaceColumn; use super::aggregate_selector::*; use super::expression_toolbar::*; -use super::InPlaceColumn; use crate::components::column_selector::{EmptyColumn, InvalidColumn}; use crate::components::type_icon::TypeIcon; use crate::components::viewer::ColumnLocator; @@ -144,7 +145,9 @@ impl ActiveColumnProps { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(self.update_and_render(config)); + self.update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); } } @@ -213,7 +216,11 @@ impl Component for ActiveColumn { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + true }, New(InPlaceColumn::Expression(col)) => { @@ -231,7 +238,11 @@ impl Component for ActiveColumn { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + true }, } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs index 25a0394087..6843086226 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs @@ -11,6 +11,7 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use perspective_client::config::*; +use perspective_client::utils::PerspectiveResultExt; use yew::prelude::*; use crate::components::containers::select::*; @@ -117,7 +118,10 @@ impl AggregateSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(config)); + ctx.props() + .update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); } pub fn get_dropdown_aggregates(&self, ctx: &Context) -> Vec> { diff --git a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs index deaf962f9a..38425dc5a6 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs @@ -14,12 +14,13 @@ use std::collections::HashSet; use std::rc::Rc; use perspective_client::config::*; +use perspective_client::utils::PerspectiveResultExt; use yew::prelude::*; +use super::InPlaceColumn; use super::filter_column::*; use super::pivot_column::*; use super::sort_column::*; -use super::InPlaceColumn; use crate::components::containers::dragdrop_list::*; use crate::components::style::LocalStyle; use crate::custom_elements::{ColumnDropDownElement, FilterDropDownElement}; @@ -233,7 +234,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(config)); + ctx.props() + .update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -245,7 +250,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(config)); + ctx.props() + .update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -257,7 +266,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(config)); + ctx.props() + .update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -270,7 +283,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(config)); + ctx.props() + .update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -285,7 +302,12 @@ impl Component for ConfigSelector { effect, &ctx.props().renderer.metadata(), ); - ApiFuture::spawn(ctx.props().update_and_render(update)); + + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -305,7 +327,10 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); ctx.props().onselect.emit(()); false }, @@ -340,7 +365,11 @@ impl Component for ConfigSelector { } }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + false }, ConfigSelectorMsg::New(DragTarget::GroupBy, InPlaceColumn::Column(col)) => { @@ -351,7 +380,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -363,7 +396,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -381,7 +418,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -393,7 +434,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -407,7 +452,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -421,7 +470,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -443,7 +496,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, @@ -459,7 +516,11 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + ctx.props().onselect.emit(()); false }, diff --git a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs index 7cb58bd324..3596cdf078 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs @@ -13,8 +13,9 @@ use std::collections::HashSet; use chrono::{Datelike, NaiveDate, TimeZone, Utc}; -use perspective_client::config::*; use perspective_client::ColumnType; +use perspective_client::config::*; +use perspective_client::utils::PerspectiveResultExt; use wasm_bindgen::JsCast; use web_sys::*; use yew::prelude::*; @@ -140,7 +141,9 @@ impl FilterColumnProps { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(self.update_and_render(update)); + self.update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); } /// Update the filter value from the string input read from the DOM. @@ -210,7 +213,9 @@ impl FilterColumnProps { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(self.update_and_render(update)); + self.update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); } } } @@ -255,7 +260,7 @@ impl Component for FilterColumn { ctx.props().filter_dropdown.autocomplete( column, if ctx.props().filter.op() == "in" || ctx.props().filter.op() == "not in" { - input.split(',').last().unwrap().to_owned() + input.split(',').next_back().unwrap().to_owned() } else { input.clone() }, diff --git a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs index 7116e7efb6..6c681d28d4 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs @@ -11,8 +11,9 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use itertools::Itertools; -use perspective_client::config::*; use perspective_client::ColumnType; +use perspective_client::config::*; +use perspective_client::utils::PerspectiveResultExt; use web_sys::*; use yew::prelude::*; @@ -97,7 +98,9 @@ impl InactiveColumnProps { ..ViewConfigUpdate::default() }; - ApiFuture::spawn(self.update_and_render(config)); + self.update_and_render(config) + .map(ApiFuture::spawn) + .unwrap_or_log(); } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs index 290b8355c9..db3b5cd37b 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs @@ -11,7 +11,7 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use perspective_client::config::*; -// use perspective_client::ColumnType; +use perspective_client::utils::PerspectiveResultExt; use web_sys::*; use yew::prelude::*; @@ -75,7 +75,12 @@ impl Component for SortColumn { sort: Some(sort), ..ViewConfigUpdate::default() }; - ApiFuture::spawn(ctx.props().update_and_render(update)); + + ctx.props() + .update_and_render(update) + .map(ApiFuture::spawn) + .unwrap_or_log(); + false }, } diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/attributes_tab.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/attributes_tab.rs index 410c5d17b3..f4f18a7101 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/attributes_tab.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/attributes_tab.rs @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use yew::{function_component, html, Html, Properties}; +use yew::{Html, Properties, function_component, html}; use super::save_settings::{SaveSettings, SaveSettingsProps}; use crate::components::expression_editor::{ExpressionEditor, ExpressionEditorProps}; diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/save_settings.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/save_settings.rs index 65b7897b2c..46067c1e06 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/save_settings.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/save_settings.rs @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use yew::{function_component, html, Callback, Html, Properties}; +use yew::{Callback, Html, Properties, function_component, html}; #[derive(Properties, PartialEq, Clone)] pub struct SaveSettingsProps { diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/sidebar.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/sidebar.rs index 4e10001887..f1985af85b 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/sidebar.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/sidebar.rs @@ -15,9 +15,10 @@ use std::rc::Rc; use derivative::Derivative; use itertools::Itertools; -use perspective_client::config::Expression; use perspective_client::ColumnType; -use yew::{html, Callback, Component, Html, Properties}; +use perspective_client::config::Expression; +use perspective_client::utils::PerspectiveResultExt; +use yew::{Callback, Component, Html, Properties, html}; use super::attributes_tab::AttributesTabProps; use super::style_tab::StyleTabProps; @@ -278,7 +279,11 @@ impl Component for ColumnSettingsSidebar { ColumnLocator::Expression(name) => { ctx.props().update_expr(name.clone(), new_expr) }, - ColumnLocator::NewExpression => ctx.props().save_expr(new_expr), + ColumnLocator::NewExpression => { + if let Err(_err) = ctx.props().save_expr(new_expr) { + tracing::warn!("Errflerr!"); + } + }, } self.initial_expr_value.clone_from(&self.expr_value); @@ -290,8 +295,9 @@ impl Component for ColumnSettingsSidebar { }, ColumnSettingsMsg::OnDelete(()) => { if ctx.props().selected_column.is_saved_expr() { - ctx.props().delete_expr(&self.column_name); + ctx.props().delete_expr(&self.column_name).unwrap_or_log(); } + ctx.props().on_close.emit(()); true }, diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs index 76e8b2788f..c1e400a415 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs @@ -14,9 +14,9 @@ pub mod stub; mod symbol; use itertools::Itertools; -use perspective_client::{clone, ColumnType}; +use perspective_client::{ColumnType, clone}; use perspective_js::utils::*; -use yew::{function_component, html, Html, Properties}; +use yew::{Html, Properties, function_component, html}; use crate::components::column_settings_sidebar::style_tab::stub::Stub; use crate::components::column_settings_sidebar::style_tab::symbol::SymbolStyle; diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/stub.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/stub.rs index 6279cb5369..166c9e3aa4 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/stub.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/stub.rs @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use yew::{function_component, html, Html, Properties}; +use yew::{Html, Properties, function_component, html}; #[derive(Properties, PartialEq)] pub struct StubProps { diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol.rs index f07abc848e..6b0f4ed110 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol.rs @@ -19,7 +19,7 @@ use std::collections::HashMap; use std::rc::Rc; use itertools::Itertools; -use yew::{html, Callback, Html, Properties}; +use yew::{Callback, Html, Properties, html}; use self::symbol_config::SymbolKVPair; use crate::components::column_settings_sidebar::style_tab::symbol::symbol_pairs::PairsList; diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs index 48315c15d2..08836cf720 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/row_selector.rs @@ -15,7 +15,7 @@ use std::rc::Rc; use itertools::Itertools; use perspective_client::clone; -use yew::{function_component, html, Html, Properties}; +use yew::{Html, Properties, function_component, html}; use super::symbol_config::SymbolKVPair; use crate::components::empty_row::EmptyRow; diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs index 2031f3db27..f668f3ddb8 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_pairs.rs @@ -13,7 +13,7 @@ use std::rc::Rc; use itertools::Itertools; -use yew::{html, Callback, Html, Properties}; +use yew::{Callback, Html, Properties, html}; use super::symbol_config::SymbolKVPair; use crate::components::column_settings_sidebar::style_tab::symbol::row_selector::RowSelector; diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs index 2ec79fa25e..a705eee493 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab/symbol/symbol_selector.rs @@ -11,7 +11,7 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use itertools::Itertools; -use yew::{function_component, html, Callback, Html, Properties}; +use yew::{Callback, Html, Properties, function_component, html}; use crate::components::containers::select::{Select, SelectItem}; diff --git a/rust/perspective-viewer/src/rust/components/containers/dragdrop_list.rs b/rust/perspective-viewer/src/rust/components/containers/dragdrop_list.rs index cbb0bf944c..fd16325193 100644 --- a/rust/perspective-viewer/src/rust/components/containers/dragdrop_list.rs +++ b/rust/perspective-viewer/src/rust/components/containers/dragdrop_list.rs @@ -224,7 +224,7 @@ where let is_duplicate = columns .iter() - .position(|x| x.1 .1.as_ref().unwrap().props.get_item() == *column); + .position(|x| x.1.1.as_ref().unwrap().props.get_item() == *column); valid_duplicate_drag = is_duplicate.is_some() && !ctx.props().allow_duplicates; if let Some(duplicate) = is_duplicate { diff --git a/rust/perspective-viewer/src/rust/components/containers/sidebar.rs b/rust/perspective-viewer/src/rust/components/containers/sidebar.rs index f32beec650..8121217ad8 100644 --- a/rust/perspective-viewer/src/rust/components/containers/sidebar.rs +++ b/rust/perspective-viewer/src/rust/components/containers/sidebar.rs @@ -13,8 +13,8 @@ use perspective_client::clone; use web_sys::Element; use yew::{ - function_component, html, use_effect_with, use_node_ref, use_state_eq, AttrValue, Callback, - Children, Html, Properties, + AttrValue, Callback, Children, Html, Properties, function_component, html, use_effect_with, + use_node_ref, use_state_eq, }; use crate::components::editable_header::{EditableHeader, EditableHeaderProps}; diff --git a/rust/perspective-viewer/src/rust/components/containers/split_panel.rs b/rust/perspective-viewer/src/rust/components/containers/split_panel.rs index 121223424a..822ff0e141 100644 --- a/rust/perspective-viewer/src/rust/components/containers/split_panel.rs +++ b/rust/perspective-viewer/src/rust/components/containers/split_panel.rs @@ -13,8 +13,8 @@ use std::cmp::max; use perspective_js::utils::global; -use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; use web_sys::HtmlElement; use yew::html::Scope; use yew::prelude::*; diff --git a/rust/perspective-viewer/src/rust/components/containers/tab_list.rs b/rust/perspective-viewer/src/rust/components/containers/tab_list.rs index 5b5600f989..450e056283 100644 --- a/rust/perspective-viewer/src/rust/components/containers/tab_list.rs +++ b/rust/perspective-viewer/src/rust/components/containers/tab_list.rs @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use yew::{classes, html, Callback, Children, Component, Html, Properties}; +use yew::{Callback, Children, Component, Html, Properties, classes, html}; use crate::components::style::LocalStyle; use crate::css; diff --git a/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs b/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs index 7cb8a1c894..075ce24aed 100644 --- a/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs +++ b/rust/perspective-viewer/src/rust/components/containers/trap_door_panel.rs @@ -10,7 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use perspective_client::clone; use yew::*; #[derive(Properties)] @@ -32,17 +31,14 @@ impl PartialEq for TrapDoorPanelProps { pub fn trap_door_panel(props: &TrapDoorPanelProps) -> Html { let sizer = use_node_ref(); let width = use_state_eq(|| 0.0); - use_effect({ - clone!(width, sizer); - move || { - width.set( - sizer - .cast::() - .unwrap() - .get_bounding_client_rect() - .width(), - ) - } + use_effect_with((width.setter(), sizer.clone()), |(width, sizer)| { + width.set( + sizer + .cast::() + .unwrap() + .get_bounding_client_rect() + .width(), + ) }); html! { diff --git a/rust/perspective-viewer/src/rust/components/editable_header.rs b/rust/perspective-viewer/src/rust/components/editable_header.rs index 305f86a38e..f9075a71fb 100644 --- a/rust/perspective-viewer/src/rust/components/editable_header.rs +++ b/rust/perspective-viewer/src/rust/components/editable_header.rs @@ -15,7 +15,7 @@ use std::rc::Rc; use derivative::Derivative; use itertools::Itertools; use web_sys::{FocusEvent, HtmlInputElement, KeyboardEvent}; -use yew::{classes, html, Callback, Component, Html, NodeRef, Properties, TargetCast}; +use yew::{Callback, Component, Html, NodeRef, Properties, TargetCast, classes, html}; use super::type_icon::TypeIconType; use crate::components::type_icon::TypeIcon; diff --git a/rust/perspective-viewer/src/rust/components/error_message.rs b/rust/perspective-viewer/src/rust/components/error_message.rs new file mode 100644 index 0000000000..29049a85d8 --- /dev/null +++ b/rust/perspective-viewer/src/rust/components/error_message.rs @@ -0,0 +1,56 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +use perspective_client::clone; +use yew::prelude::*; +use yew::{Properties, function_component, html}; + +use crate::components::style::LocalStyle; +use crate::css; +use crate::session::Session; +use crate::utils::AddListener; + +#[derive(PartialEq, Properties)] +pub struct ErrorMessageProps { + pub session: Session, +} + +#[function_component(ErrorMessage)] +pub fn error_message(p: &ErrorMessageProps) -> yew::Html { + let error = use_state(|| p.session.get_error()); + use_effect_with( + (error.setter(), p.session.clone()), + |(set_error, session)| { + let sub = session.table_errored.add_listener({ + clone!(set_error); + move |y| set_error.set(y) + }); + + || drop(sub) + }, + ); + tracing::error!("Fucked"); + html! { + <> + +
+ + + if let Some(msg) = error.as_ref() { { msg } } + +
+ + } +} diff --git a/rust/perspective-viewer/src/rust/components/export_dropdown.rs b/rust/perspective-viewer/src/rust/components/export_dropdown.rs index b8dbccd18c..5b7458ccc7 100644 --- a/rust/perspective-viewer/src/rust/components/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/export_dropdown.rs @@ -83,7 +83,7 @@ fn get_menu_items(name: &str, has_render: bool) -> Vec { ExportMethod::ArrowAll.new_file(name), ]), ExportDropDownMenuItem::OptGroup("Config".into(), vec![ - ExportMethod::JsonConfig.new_file(name) + ExportMethod::JsonConfig.new_file(name), ]), ] } diff --git a/rust/perspective-viewer/src/rust/components/expression_editor.rs b/rust/perspective-viewer/src/rust/components/expression_editor.rs index 03207c6fe4..d581beb6e2 100644 --- a/rust/perspective-viewer/src/rust/components/expression_editor.rs +++ b/rust/perspective-viewer/src/rust/components/expression_editor.rs @@ -12,7 +12,7 @@ use std::rc::Rc; -use perspective_client::{clone, ExprValidationError}; +use perspective_client::{ExprValidationError, clone}; use yew::prelude::*; use super::form::code_editor::*; diff --git a/rust/perspective-viewer/src/rust/components/font_loader.rs b/rust/perspective-viewer/src/rust/components/font_loader.rs index 5d81b701bf..7da9ef01c9 100644 --- a/rust/perspective-viewer/src/rust/components/font_loader.rs +++ b/rust/perspective-viewer/src/rust/components/font_loader.rs @@ -12,13 +12,13 @@ use std::cell::{Cell, Ref, RefCell}; use std::future::Future; -use std::iter::{repeat_with, Iterator}; +use std::iter::{Iterator, repeat_with}; use std::rc::Rc; use futures::future::{join_all, select_all}; use perspective_js::utils::{global, *}; -use wasm_bindgen::prelude::*; use wasm_bindgen::JsCast; +use wasm_bindgen::prelude::*; use wasm_bindgen_futures::JsFuture; use yew::prelude::*; @@ -191,7 +191,10 @@ impl FontLoaderProps { // An async task which times out. Can be used to timeout an optional async task // by combinging with `Promise::any`. -fn timeout_font_task(family: &str, weight: &str) -> impl Future> { +fn timeout_font_task( + family: &str, + weight: &str, +) -> impl Future> + use<> { let timeout_msg = format!("Timeout awaiting font \"{}:{}\"", family, weight); async { set_timeout(FONT_DOWNLOAD_TIMEOUT_MS).await?; diff --git a/rust/perspective-viewer/src/rust/components/form/code_editor.rs b/rust/perspective-viewer/src/rust/components/form/code_editor.rs index bf8547b02a..9915b8fe4e 100644 --- a/rust/perspective-viewer/src/rust/components/form/code_editor.rs +++ b/rust/perspective-viewer/src/rust/components/form/code_editor.rs @@ -20,7 +20,7 @@ use yew::prelude::*; use crate::components::form::highlight::highlight; use crate::components::style::LocalStyle; use crate::custom_elements::FunctionDropDownElement; -use crate::exprtk::{tokenize, Cursor}; +use crate::exprtk::{Cursor, tokenize}; use crate::utils::*; use crate::*; diff --git a/rust/perspective-viewer/src/rust/components/form/debug.rs b/rust/perspective-viewer/src/rust/components/form/debug.rs index 7a4fe99000..3eed178fb8 100644 --- a/rust/perspective-viewer/src/rust/components/form/debug.rs +++ b/rust/perspective-viewer/src/rust/components/form/debug.rs @@ -19,7 +19,7 @@ use yew::prelude::*; use crate::components::containers::trap_door_panel::TrapDoorPanel; use crate::components::form::code_editor::CodeEditor; use crate::components::style::LocalStyle; -use crate::js::{copy_to_clipboard, paste_from_clipboard, MimeType}; +use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard}; use crate::model::*; use crate::presentation::*; use crate::renderer::*; @@ -56,7 +56,7 @@ impl DebugPanelProps { text: UseStateSetter>, error: UseStateSetter>, modified: UseStateSetter, - ) -> impl Fn(()) { + ) -> impl Fn(()) + use<> { let props = self.clone(); move |_| { error.set(None); @@ -118,17 +118,32 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { move |(text, props)| { props.set_text(text.clone()); error.set(None); - let sub1 = props.renderer().style_changed.add_listener({ - props.reset_callback(text.clone(), error.setter(), modified.setter()) - }); - - let sub2 = props.renderer().reset_changed.add_listener({ - props.reset_callback(text.clone(), error.setter(), modified.setter()) - }); - - let sub3 = props.session().view_config_changed.add_listener({ - props.reset_callback(text.clone(), error.setter(), modified.setter()) - }); + let sub1 = props + .renderer() + .style_changed + .add_listener(props.reset_callback( + text.clone(), + error.setter(), + modified.setter(), + )); + + let sub2 = props + .renderer() + .reset_changed + .add_listener(props.reset_callback( + text.clone(), + error.setter(), + modified.setter(), + )); + + let sub3 = props + .session() + .view_config_changed + .add_listener(props.reset_callback( + text.clone(), + error.setter(), + modified.setter(), + )); || { drop(sub1); diff --git a/rust/perspective-viewer/src/rust/components/form/number_field.rs b/rust/perspective-viewer/src/rust/components/form/number_field.rs index 6fe446225c..2b49d5f5aa 100644 --- a/rust/perspective-viewer/src/rust/components/form/number_field.rs +++ b/rust/perspective-viewer/src/rust/components/form/number_field.rs @@ -11,7 +11,7 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use web_sys::{HtmlInputElement, InputEvent}; -use yew::{function_component, html, Callback, Properties, TargetCast}; +use yew::{Callback, Properties, TargetCast, function_component, html}; use crate::components::form::optional_field::OptionalField; diff --git a/rust/perspective-viewer/src/rust/components/form/number_range_field.rs b/rust/perspective-viewer/src/rust/components/form/number_range_field.rs index 4dcc90107e..4826aebb91 100644 --- a/rust/perspective-viewer/src/rust/components/form/number_range_field.rs +++ b/rust/perspective-viewer/src/rust/components/form/number_range_field.rs @@ -11,7 +11,7 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use web_sys::HtmlInputElement; -use yew::{function_component, html, use_callback, use_node_ref, Callback, Properties}; +use yew::{Callback, Properties, function_component, html, use_callback, use_node_ref}; use crate::components::form::optional_field::OptionalField; diff --git a/rust/perspective-viewer/src/rust/components/form/optional_field.rs b/rust/perspective-viewer/src/rust/components/form/optional_field.rs index 01fb512582..10e2868233 100644 --- a/rust/perspective-viewer/src/rust/components/form/optional_field.rs +++ b/rust/perspective-viewer/src/rust/components/form/optional_field.rs @@ -10,7 +10,7 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use yew::{classes, function_component, html, Callback, Children, Html, MouseEvent, Properties}; +use yew::{Callback, Children, Html, MouseEvent, Properties, classes, function_component, html}; #[derive(Properties, PartialEq)] pub struct OptionalFieldProps { diff --git a/rust/perspective-viewer/src/rust/components/form/select_field.rs b/rust/perspective-viewer/src/rust/components/form/select_field.rs index 00d4dcdf36..7cb890333d 100644 --- a/rust/perspective-viewer/src/rust/components/form/select_field.rs +++ b/rust/perspective-viewer/src/rust/components/form/select_field.rs @@ -15,7 +15,7 @@ use std::sync::Arc; use itertools::Itertools; use strum::IntoEnumIterator; -use yew::{function_component, html, Callback, Properties}; +use yew::{Callback, Properties, function_component, html}; use crate::components::containers::select::{Select, SelectItem}; use crate::components::form::optional_field::OptionalField; diff --git a/rust/perspective-viewer/src/rust/components/mod.rs b/rust/perspective-viewer/src/rust/components/mod.rs index 04d2c9c853..32396839e4 100644 --- a/rust/perspective-viewer/src/rust/components/mod.rs +++ b/rust/perspective-viewer/src/rust/components/mod.rs @@ -19,6 +19,7 @@ pub mod column_selector; pub mod containers; pub mod copy_dropdown; pub mod datetime_column_style; +pub mod error_message; pub mod export_dropdown; pub mod expression_editor; pub mod filter_dropdown; @@ -31,6 +32,7 @@ pub mod plugin_selector; pub mod render_warning; pub mod status_bar; pub mod status_bar_counter; +pub mod status_indicator; pub mod string_column_style; pub mod style; pub mod type_icon; diff --git a/rust/perspective-viewer/src/rust/components/plugin_selector.rs b/rust/perspective-viewer/src/rust/components/plugin_selector.rs index 5abb3643a3..fee9e5b344 100644 --- a/rust/perspective-viewer/src/rust/components/plugin_selector.rs +++ b/rust/perspective-viewer/src/rust/components/plugin_selector.rs @@ -77,20 +77,28 @@ impl Component for PluginSelector { match msg { RendererSelectPlugin(_plugin_name) => true, ComponentSelectPlugin(plugin_name) => { - ctx.props() - .renderer - .update_plugin(&PluginUpdate::Update(plugin_name)) - .unwrap(); - - let mut update = ViewConfigUpdate::default(); - ctx.props() - .session - .set_update_column_defaults(&mut update, &ctx.props().renderer.metadata()); - - ctx.props().presentation.set_open_column_settings(None); - ApiFuture::spawn(ctx.props().update_and_render(update)); - self.is_open = false; - false + if ctx.props().session.get_error().is_none() { + ctx.props() + .renderer + .update_plugin(&PluginUpdate::Update(plugin_name)) + .unwrap(); + + let mut update = ViewConfigUpdate::default(); + ctx.props() + .session + .set_update_column_defaults(&mut update, &ctx.props().renderer.metadata()); + + if let Ok(task) = ctx.props().update_and_render(update) { + ApiFuture::spawn(task); + } + + ctx.props().presentation.set_open_column_settings(None); + self.is_open = false; + false + } else { + self.is_open = false; + true + } }, OpenMenu => { self.is_open = !self.is_open; diff --git a/rust/perspective-viewer/src/rust/components/render_warning.rs b/rust/perspective-viewer/src/rust/components/render_warning.rs index 096d09432c..fe8b5166fc 100644 --- a/rust/perspective-viewer/src/rust/components/render_warning.rs +++ b/rust/perspective-viewer/src/rust/components/render_warning.rs @@ -44,13 +44,13 @@ impl RenderWarning { fn update_warnings(&mut self, ctx: &Context) { if let Some((num_cols, num_rows, max_cols, max_rows)) = ctx.props().dimensions { let count = num_cols * num_rows; - if max_cols.map_or(false, |x| x < num_cols) { + if max_cols.is_some_and(|x| x < num_cols) { self.col_warn = Some((max_cols.unwrap(), num_cols)); } else { self.col_warn = None; } - if max_rows.map_or(false, |x| x < num_rows) { + if max_rows.is_some_and(|x| x < num_rows) { self.row_warn = Some((num_cols * max_rows.unwrap(), count)); } else { self.row_warn = None; diff --git a/rust/perspective-viewer/src/rust/components/status_bar.rs b/rust/perspective-viewer/src/rust/components/status_bar.rs index 87b7b6ec88..8112bd2076 100644 --- a/rust/perspective-viewer/src/rust/components/status_bar.rs +++ b/rust/perspective-viewer/src/rust/components/status_bar.rs @@ -14,6 +14,7 @@ use wasm_bindgen::JsCast; use web_sys::*; use yew::prelude::*; +use super::status_indicator::StatusIndicator; use super::style::LocalStyle; use crate::components::containers::select::*; use crate::components::status_bar_counter::StatusBarRowsCounter; @@ -27,7 +28,7 @@ use crate::utils::WeakScope; use crate::utils::*; use crate::*; -#[derive(Properties)] +#[derive(Properties, Clone)] pub struct StatusBarProps { pub id: String, pub on_reset: Callback, @@ -52,10 +53,12 @@ pub enum StatusBarMsg { Reset(bool), Export, Copy, + Noop, SetThemeConfig((Vec, Option)), SetTheme(String), - TableStatsChanged, - SetIsUpdating(bool), + // SetError(Option), + // TableStatsChanged, + // SetIsUpdating(bool), SetTitle(Option), } @@ -66,7 +69,7 @@ pub struct StatusBar { themes: Vec, export_ref: NodeRef, copy_ref: NodeRef, - _sub: [Subscription; 5], + _sub: [Subscription; 2], } impl Component for StatusBar { @@ -75,18 +78,6 @@ impl Component for StatusBar { fn create(ctx: &Context) -> Self { let _sub = [ - ctx.props() - .session - .stats_changed - .add_listener(ctx.link().callback(|_| StatusBarMsg::TableStatsChanged)), - ctx.props() - .session - .view_config_changed - .add_listener(ctx.link().callback(|_| StatusBarMsg::SetIsUpdating(true))), - ctx.props() - .session - .view_created - .add_listener(ctx.link().callback(|_| StatusBarMsg::SetIsUpdating(false))), ctx.props() .presentation .theme_config_updated @@ -94,7 +85,7 @@ impl Component for StatusBar { ctx.props() .presentation .title_changed - .add_listener(ctx.link().callback(|_| StatusBarMsg::TableStatsChanged)), + .add_listener(ctx.link().callback(|_| StatusBarMsg::Noop)), ]; // Fetch initial theme @@ -117,11 +108,6 @@ impl Component for StatusBar { fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - StatusBarMsg::SetIsUpdating(is_updating) => { - self.is_updating = max!(0, self.is_updating + if is_updating { 1 } else { -1 }); - true - }, - StatusBarMsg::TableStatsChanged => true, StatusBarMsg::Reset(all) => { ctx.props().on_reset.emit(all); false @@ -157,6 +143,7 @@ impl Component for StatusBar { CopyDropDownMenuElement::new_from_model(ctx.props()).open(target); false }, + StatusBarMsg::Noop => true, StatusBarMsg::SetTitle(title) => { ctx.props().presentation.set_title(title); false @@ -165,7 +152,6 @@ impl Component for StatusBar { } fn view(&self, ctx: &Context) -> Html { - let stats = ctx.props().session.get_table_stats(); let mut is_updating_class_name = classes!(); if self.is_updating > 0 { is_updating_class_name.push("updating") @@ -181,7 +167,6 @@ impl Component for StatusBar { let export = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Export); let copy = ctx.link().callback(|_: MouseEvent| StatusBarMsg::Copy); - let theme_button = match &self.theme { None => html! {}, Some(selected) => { @@ -232,11 +217,19 @@ impl Component for StatusBar { && (ctx.props().presentation.is_settings_open() || ctx.props().presentation.get_title().is_some()); + if !ctx.props().session.has_table() { + is_updating_class_name.push("updating"); + } + html! { <>
- 0}> + + if is_menu { - + } +
if is_menu {