diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..2d17ada --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,6 @@ +# Configuration for wasm32-unknown-unknown target (browsers, wasm-pack) +[target.wasm32-unknown-unknown] +rustflags = ['--cfg', 'getrandom_backend="wasm_js"', '-C', 'target-feature=+bulk-memory,+mutable-globals'] + +[unstable] +build-std = ["panic_abort", "std"] diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 7f232bb..f0394d0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ .zed +.direnv diff --git a/Cargo.lock b/Cargo.lock index 23adebe..d7ee755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,23 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "accessory" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28e416a3ab45838bac2ab2d81b1088d738d7b2d2c5272a54d39366565a29bd80" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "addr2line" -version = "0.24.2" +version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ "gimli", ] @@ -26,12 +38,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android-tzdata" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" - [[package]] name = "android_system_properties" version = "0.1.5" @@ -75,6 +81,12 @@ dependencies = [ "syn", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.5.0" @@ -107,9 +119,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.4" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5" +checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871" dependencies = [ "axum-core", "base64 0.22.1", @@ -127,15 +139,14 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rustversion", - "serde", + "serde_core", "serde_json", "serde_path_to_error", "serde_urlencoded", "sha1", "sync_wrapper", "tokio", - "tokio-tungstenite 0.26.2", + "tokio-tungstenite 0.28.0", "tower", "tower-layer", "tower-service", @@ -144,9 +155,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.5.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6" +checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", @@ -155,7 +166,6 @@ dependencies = [ "http-body-util", "mime", "pin-project-lite", - "rustversion", "sync_wrapper", "tower-layer", "tower-service", @@ -164,9 +174,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.75" +version = "0.3.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" dependencies = [ "addr2line", "cfg-if", @@ -174,7 +184,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-link", ] [[package]] @@ -191,9 +201,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" [[package]] name = "block-buffer" @@ -246,18 +256,19 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.27" +version = "1.2.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" +checksum = "e1d05d92f4b1fd76aad469d46cdd858ca761576082cd37df81416691e50199fb" dependencies = [ + "find-msvc-tools", "shlex", ] [[package]] name = "cfg-expr" -version = "0.20.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d0390889d58f934f01cd49736275b4c2da15bcfc328c78ff2349907e6cabf22" +checksum = "1a2c5f3bf25ec225351aa1c8e230d04d880d3bd89dea133537dafad4ae291e5c" dependencies = [ "smallvec", "target-lexicon", @@ -265,9 +276,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -277,11 +288,10 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "chrono" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" dependencies = [ - "android-tzdata", "iana-time-zone", "js-sys", "num-traits", @@ -298,6 +308,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + [[package]] name = "const-oid" version = "0.10.1" @@ -321,9 +341,9 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" dependencies = [ "cfg-if", ] @@ -378,6 +398,41 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" +[[package]] +name = "delegate-display" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9926686c832494164c33a36bf65118f4bd6e704000b58c94681bf62e9ad67a74" +dependencies = [ + "impartial-ord", + "itoa", + "macroific", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "digest" version = "0.10.7" @@ -424,12 +479,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -463,17 +518,35 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fancy_constructor" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28a27643a5d05f3a22f5afd6e0d0e6e354f92d37907006f97b84b9cb79082198" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "fastrand" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "find-msvc-tools" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0399f9d26e5191ce32c498bebd31e7a3ceabc2745f0ac54af3f335126c3f24b3" + [[package]] name = "flate2" -version = "1.1.2" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" +checksum = "dc5a4e564e38c699f2880d3fda590bedc2e69f3f84cd48b457bd892ce61d0aa9" dependencies = [ "crc32fast", "miniz_oxide", @@ -487,9 +560,9 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -600,22 +673,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", - "wasi 0.14.2+wasi-0.2.4", + "wasi 0.14.7+wasi-0.2.4", + "wasm-bindgen", ] [[package]] name = "gimli" -version = "0.31.1" +version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "gio" -version = "0.20.12" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e27e276e7b6b8d50f6376ee7769a71133e80d093bdc363bd0af71664228b831" +checksum = "ed68efc12b748a771be2dccc49480d8584004382967c98323245fc3c38b74a42" dependencies = [ "futures-channel", "futures-core", @@ -630,22 +705,22 @@ dependencies = [ [[package]] name = "gio-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +checksum = "171ed2f6dd927abbe108cfd9eebff2052c335013f5879d55bab0dc1dee19b706" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "glib" -version = "0.20.12" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +checksum = "e1f2cbc4577536c849335878552f42086bfd25a8dcd6f54a18655cf818b20c8f" dependencies = [ "bitflags", "futures-channel", @@ -664,9 +739,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.12" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +checksum = "55eda916eecdae426d78d274a17b48137acdca6fba89621bd3705f2835bc719f" dependencies = [ "heck", "proc-macro-crate", @@ -677,9 +752,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +checksum = "d09d3d0fddf7239521674e57b0465dfbd844632fec54f059f7f56112e3f927e1" dependencies = [ "libc", "system-deps", @@ -687,9 +762,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.20.10" +version = "0.21.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +checksum = "538e41d8776173ec107e7b0f2aceced60abc368d7e1d81c1f0e2ecd35f59080d" dependencies = [ "glib-sys", "libc", @@ -698,9 +773,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" [[package]] name = "heck" @@ -781,19 +856,21 @@ dependencies = [ [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "http", "http-body", "httparse", "httpdate", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -801,9 +878,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ "base64 0.22.1", "bytes", @@ -825,9 +902,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.63" +version = "0.1.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -935,9 +1012,9 @@ dependencies = [ [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -954,17 +1031,62 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "impartial-ord" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab604ee7085efba6efc65e4ebca0e9533e3aff6cb501d7d77b211e3a781c6d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "indenter" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexed_db_futures" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ff41758cbd104e91033bb53bc449bec7eea65652960c81eddf3fc146ecea19" +dependencies = [ + "accessory", + "cfg-if", + "delegate-display", + "derive_more", + "fancy_constructor", + "indexed_db_futures_macros_internal", + "js-sys", + "sealed", + "smallvec", + "thiserror", + "tokio", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "indexed_db_futures_macros_internal" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caeba94923b68f254abef921cea7e7698bf4675fdd89d7c58bf1ed885b49a27d" +dependencies = [ + "macroific", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown", @@ -972,9 +1094,9 @@ dependencies = [ [[package]] name = "io-uring" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ "bitflags", "cfg-if", @@ -1014,9 +1136,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "js-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +checksum = "ec48937a97411dcb524a265206ccd4c90bb711fca92b2792c407f268825b9305" dependencies = [ "once_cell", "wasm-bindgen", @@ -1036,15 +1158,15 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.174" +version = "0.2.176" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litemap" @@ -1054,17 +1176,65 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "log" -version = "0.4.27" +version = "0.4.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" + +[[package]] +name = "macroific" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89f276537b4b8f981bf1c13d79470980f71134b7bdcc5e6e911e910e556b0285" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "macroific_macro", +] + +[[package]] +name = "macroific_attr_parse" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad4023761b45fcd36abed8fb7ae6a80456b0a38102d55e89a57d9a594a236be9" +dependencies = [ + "proc-macro2", + "quote", + "sealed", + "syn", +] + +[[package]] +name = "macroific_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "d0a7594d3c14916fa55bef7e9d18c5daa9ed410dd37504251e4b75bbdeec33e3" +dependencies = [ + "proc-macro2", + "quote", + "sealed", + "syn", +] + +[[package]] +name = "macroific_macro" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4da6f2ed796261b0a74e2b52b42c693bb6dee1effba3a482c49592659f824b3b" +dependencies = [ + "macroific_attr_parse", + "macroific_core", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "matchers" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" dependencies = [ - "regex-automata 0.1.10", + "regex-automata", ] [[package]] @@ -1075,9 +1245,9 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mime" @@ -1087,9 +1257,9 @@ checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "minicbor" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50699691433ccd88fbe1f11e9155691d71fc363595109f35a92b77be2e0158f6" +checksum = "734daad4ff3b880f23dc2a675dd74553fa8e583367aa7523f96a16e96a516b62" dependencies = [ "minicbor-derive", ] @@ -1112,6 +1282,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", + "simd-adler32", ] [[package]] @@ -1127,12 +1298,11 @@ dependencies = [ [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -1146,9 +1316,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.7" +version = "0.37.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" dependencies = [ "memchr", ] @@ -1159,12 +1329,6 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking" version = "2.2.1" @@ -1173,9 +1337,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1197,9 +1361,9 @@ checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "potential_utf" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +checksum = "84df19adbe5b5a0782edcab45899906947ab039ccf4573713735ee7de1e6b08a" dependencies = [ "zerovec", ] @@ -1215,27 +1379,27 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit", + "toml_edit 0.23.6", ] [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] @@ -1248,9 +1412,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", @@ -1277,9 +1441,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" dependencies = [ "either", "rayon-core", @@ -1287,63 +1451,36 @@ dependencies = [ [[package]] name = "rayon-core" -version = "1.12.1" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" dependencies = [ "crossbeam-deque", "crossbeam-utils", ] -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata 0.4.9", - "regex-syntax 0.8.5", -] - [[package]] name = "regex-automata" -version = "0.1.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" -dependencies = [ - "regex-syntax 0.6.29", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +checksum = "833eb9ce86d40ef33cb1306d8accf7bc8ec2bfea4355cbdebb3df68b40925cad" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.5", + "regex-syntax", ] [[package]] name = "regex-syntax" -version = "0.6.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" - -[[package]] -name = "regex-syntax" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64 0.22.1", "bytes", @@ -1375,9 +1512,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -1387,22 +1524,22 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.0.7" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1419,10 +1556,13 @@ dependencies = [ "axum", "bytes", "chrono", + "console_error_panic_hook", "eyre", "futures", "gio", "glib", + "indexed_db_futures", + "js-sys", "rand", "rayon", "reqwest", @@ -1435,7 +1575,11 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber", + "tracing-wasm", "tungstenite 0.27.0", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] @@ -1445,7 +1589,9 @@ dependencies = [ "automerge", "base64 0.21.7", "bs58", + "getrandom", "hex", + "js-sys", "minicbor", "rand", "samod-test-harness", @@ -1456,6 +1602,8 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", + "wasm-bindgen", + "web-sys", ] [[package]] @@ -1468,20 +1616,41 @@ dependencies = [ "tracing", ] +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde" -version = "1.0.219" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.219" +version = "1.0.228" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", @@ -1490,24 +1659,26 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" dependencies = [ "itoa", "memchr", "ryu", "serde", + "serde_core", ] [[package]] name = "serde_path_to_error" -version = "0.1.17" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" dependencies = [ "itoa", "serde", + "serde_core", ] [[package]] @@ -1581,18 +1752,24 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "signal-hook-registry" -version = "1.4.5" +version = "1.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" dependencies = [ "libc", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -1612,12 +1789,12 @@ dependencies = [ [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1628,9 +1805,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1678,31 +1855,31 @@ checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" [[package]] name = "tempfile" -version = "3.20.0" +version = "3.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" dependencies = [ "fastrand", "getrandom", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" dependencies = [ "proc-macro2", "quote", @@ -1730,9 +1907,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -1745,9 +1922,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.0" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1140bb80481756a8cbe10541f37433b459c5aa1e727b4c020fbfebdc25bf3ec4" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -1759,7 +1936,7 @@ dependencies = [ "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1799,33 +1976,33 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" +checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" dependencies = [ "futures-util", "log", "tokio", - "tungstenite 0.26.2", + "tungstenite 0.27.0", ] [[package]] name = "tokio-tungstenite" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489a59b6730eda1b0171fcfda8b121f4bee2b35cba8645ca35c5f7ba3eb736c1" +checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", "tokio", - "tungstenite 0.27.0", + "tungstenite 0.28.0", ] [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -1842,8 +2019,8 @@ checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", - "toml_datetime", - "toml_edit", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", ] [[package]] @@ -1855,6 +2032,15 @@ dependencies = [ "serde", ] +[[package]] +name = "toml_datetime" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f1085dec27c2b6632b04c80b3bb1b4300d6495d1e129693bdda7d91e72eec1" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -1864,7 +2050,28 @@ dependencies = [ "indexmap", "serde", "serde_spanned", - "toml_datetime", + "toml_datetime 0.6.11", + "winnow", +] + +[[package]] +name = "toml_edit" +version = "0.23.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3effe7c0e86fdff4f69cdd2ccc1b96f933e24811c5441d44904e8683e27184b" +dependencies = [ + "indexmap", + "toml_datetime 0.7.2", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf893c33be71572e0e9aa6dd15e6677937abd686b066eac3f8cd3531688a627" +dependencies = [ "winnow", ] @@ -1960,14 +2167,14 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.19" +version = "0.3.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" dependencies = [ "matchers", "nu-ansi-term", "once_cell", - "regex", + "regex-automata", "sharded-slab", "smallvec", "thread_local", @@ -1976,6 +2183,17 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "tracing-wasm" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" +dependencies = [ + "tracing", + "tracing-subscriber", + "wasm-bindgen", +] + [[package]] name = "try-lock" version = "0.2.5" @@ -1984,9 +2202,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "tungstenite" -version = "0.26.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" +checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" dependencies = [ "bytes", "data-encoding", @@ -2001,9 +2219,9 @@ dependencies = [ [[package]] name = "tungstenite" -version = "0.27.0" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" +checksum = "8628dcc84e5a09eb3d8423d6cb682965dea9133204e8fb3efee74c2a0c259442" dependencies = [ "bytes", "data-encoding", @@ -2018,15 +2236,15 @@ dependencies = [ [[package]] name = "typenum" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" [[package]] name = "unicode-segmentation" @@ -2034,15 +2252,22 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -2059,9 +2284,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.17.0" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +checksum = "2f87b8aa10b915a06587d0dec516c282ff295b475d94abf425d62b57710070a2" dependencies = [ "getrandom", "js-sys", @@ -2104,30 +2329,40 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" -version = "0.14.2+wasi-0.2.4" +version = "0.14.7+wasi-0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" dependencies = [ - "wit-bindgen-rt", + "wasip2", +] + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", ] [[package]] name = "wasm-bindgen" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +checksum = "c1da10c01ae9f1ae40cbfac0bac3b1e724b320abfcf52229f80b547c0d250e2d" dependencies = [ "cfg-if", "once_cell", "rustversion", "wasm-bindgen-macro", + "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +checksum = "671c9a5a66f49d8a47345ab942e2cb93c7d1d0339065d4f8139c486121b43b19" dependencies = [ "bumpalo", "log", @@ -2139,9 +2374,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.50" +version = "0.4.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +checksum = "7e038d41e478cc73bae0ff9b36c60cff1c98b8f38f8d7e8061e79ee63608ac5c" dependencies = [ "cfg-if", "js-sys", @@ -2152,9 +2387,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +checksum = "7ca60477e4c59f5f2986c50191cd972e3a50d8a95603bc9434501cf156a9a119" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2162,9 +2397,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +checksum = "9f07d2f20d4da7b26400c9f4a0511e6e0345b040694e8a75bd41d578fa4421d7" dependencies = [ "proc-macro2", "quote", @@ -2175,50 +2410,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.100" +version = "0.2.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +checksum = "bad67dc8b2a1a6e5448428adec4c3e84c43e561d8c9ee8a9e5aabeb193ec41d1" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.77" +version = "0.3.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +checksum = "9367c417a924a74cae129e6a2ae3b47fabb1f8995595ab474029da749a8be120" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows-core" -version = "0.61.2" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", @@ -2229,9 +2442,9 @@ dependencies = [ [[package]] name = "windows-implement" -version = "0.60.0" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", @@ -2240,9 +2453,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.1" +version = "0.59.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", @@ -2251,24 +2464,24 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.3" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] name = "windows-result" -version = "0.3.4" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ "windows-link", ] [[package]] name = "windows-strings" -version = "0.4.2" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ "windows-link", ] @@ -2291,6 +2504,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-targets" version = "0.52.6" @@ -2357,21 +2579,18 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.7.12" +version = "0.7.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf" dependencies = [ "memchr", ] [[package]] -name = "wit-bindgen-rt" -version = "0.39.0" +name = "wit-bindgen" +version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" -dependencies = [ - "bitflags", -] +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" [[package]] name = "writeable" @@ -2405,18 +2624,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +checksum = "0894878a5fa3edfd6da3f88c4805f4c8558e2b996227a3d864f47fe11e38282c" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +checksum = "88d2b8d9c68ad2b9e4340d7832716a4d21a22a1154777ad56ea55c51a9cf3831" dependencies = [ "proc-macro2", "quote", @@ -2457,9 +2676,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..3f3560a --- /dev/null +++ b/flake.lock @@ -0,0 +1,100 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1756795219, + "narHash": "sha256-tKBQtz1JLKWrCJUxVkHKR+YKmVpm0KZdJdPWmR2slQ8=", + "owner": "nix-community", + "repo": "fenix", + "rev": "80dbdab137f2809e3c823ed027e1665ce2502d74", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1756542300, + "narHash": "sha256-tlOn88coG5fzdyqz6R93SQL5Gpq+m/DsWpekNFhqPQk=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "d7600c775f877cd87b4f5a831c28aa94137377aa", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1756597274, + "narHash": "sha256-wfaKRKsEVQDB7pQtAt04vRgFphkVscGRpSx3wG1l50E=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "21614ed2d3279a9aa1f15c88d293e65a98991b30", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..cffd4d1 --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + description = "samod"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + flake-utils, + fenix, + }: + flake-utils.lib.eachDefaultSystem ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + fenixPkgs = fenix.packages.${system}; + + rustToolchain = fenixPkgs.fromToolchainFile { + file = ./rust-toolchain.toml; + sha256 = "sha256-mqm8AFXDs+VkVDyYfMDGY7Ukhv8Y6Is9S9kt1pfnqkM="; + }; + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + # Combined Rust toolchain with WASM targets + rustToolchain + + # WebAssembly tools + wasm-pack + + # LLVM + llvmPackages.bintools + ]; + }; + } + ); +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..86e8f38 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "nightly" +components = ["rustfmt", "clippy", "rust-src"] +targets = ["wasm32-unknown-unknown"] diff --git a/samod-core/Cargo.toml b/samod-core/Cargo.toml index 6da632b..be0a988 100644 --- a/samod-core/Cargo.toml +++ b/samod-core/Cargo.toml @@ -10,7 +10,7 @@ license = "MIT" [dependencies] base64 = "0.21" tracing = "0.1.41" -uuid = { version = "1.0", features = ["v4", "serde"] } +uuid = { version = "1.0", features = ["v4", "serde", "js"] } rand = "0.9" automerge = "0.7.0" serde = { version = "1.0", features = ["derive"] } @@ -21,8 +21,21 @@ bs58 = { version = "0.5.1", features = ["check"] } sha2 = "0.10.9" [lib] +crate-type = ["cdylib", "rlib"] + +[target.'cfg(target_arch = "wasm32")'.dependencies] +js-sys = { version = "0.3", optional = true } +wasm-bindgen = { version = "0.2", optional = true } +web-sys = { version = "0.3", optional = true } +getrandom = { version = "0.3", features = ["wasm_js"] } + +[features] +default = [] +wasm = ["js-sys", "wasm-bindgen", "web-sys"] [dev-dependencies] tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } -tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } samod-test-harness = { path = "../samod-test-harness" } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } diff --git a/samod-core/src/actors/document/doc_actor_result.rs b/samod-core/src/actors/document/doc_actor_result.rs index d5d2d8f..49ed320 100644 --- a/samod-core/src/actors/document/doc_actor_result.rs +++ b/samod-core/src/actors/document/doc_actor_result.rs @@ -7,7 +7,7 @@ use crate::{ }; /// Result of processing a message or I/O completion. -#[derive(Debug)] +#[derive(Debug, Default)] pub struct DocActorResult { /// Document I/O tasks that need to be executed by the caller. pub io_tasks: Vec>, @@ -24,13 +24,7 @@ pub struct DocActorResult { impl DocActorResult { /// Creates an empty result. pub fn new() -> Self { - Self { - io_tasks: Vec::new(), - outgoing_messages: Vec::new(), - ephemeral_messages: Vec::new(), - change_events: Vec::new(), - stopped: false, - } + Self::default() } pub(crate) fn emit_ephemeral_message(&mut self, msg: Vec) { @@ -65,9 +59,3 @@ impl DocActorResult { task_id } } - -impl Default for DocActorResult { - fn default() -> Self { - Self::new() - } -} diff --git a/samod-core/src/actors/document/document_actor.rs b/samod-core/src/actors/document/document_actor.rs index d54cbc3..f8baada 100644 --- a/samod-core/src/actors/document/document_actor.rs +++ b/samod-core/src/actors/document/document_actor.rs @@ -147,8 +147,8 @@ impl DocumentActor { &self.document_id } - fn local_peer_id(&self) -> PeerId { - self.local_peer_id.clone() + fn local_peer_id(&self) -> &PeerId { + &self.local_peer_id } /// Provides mutable access to the document with automatic side effect handling. diff --git a/samod-core/src/actors/hub/connection/connection.rs b/samod-core/src/actors/hub/connection/connection.rs index 11536a7..1374118 100644 --- a/samod-core/src/actors/hub/connection/connection.rs +++ b/samod-core/src/actors/hub/connection/connection.rs @@ -5,7 +5,7 @@ use crate::{ actors::{hub::HubResults, messages::SyncMessage}, network::{ ConnDirection, ConnectionInfo, ConnectionState, PeerDocState, PeerMetadata, - wire_protocol::WireMessage, + wire_protocol::{PROTOCOL_VERSION, WireMessage}, }, }; @@ -74,7 +74,7 @@ impl Connection { created_at, WireMessage::Join { sender_id: local_peer_id.clone(), - supported_protocol_versions: vec!["1".to_string()], + supported_protocol_versions: vec![PROTOCOL_VERSION.to_string()], metadata: local_metadata.as_ref().map(|meta| meta.to_wire(None)), }, ); @@ -107,7 +107,7 @@ impl Connection { ?supported_protocol_versions, "received Join message from peer" ); - if !supported_protocol_versions.contains(&"1".to_string()) { + if !supported_protocol_versions.contains(&PROTOCOL_VERSION.to_string()) { tracing::warn!(conn_id=?self.id, "peer does not support protocol version 1"); self.send( out, @@ -125,7 +125,7 @@ impl Connection { now, WireMessage::Peer { sender_id: self.local_peer_id.clone(), - selected_protocol_version: "1".to_string(), + selected_protocol_version: PROTOCOL_VERSION.to_string(), target_id: sender_id.clone(), metadata: self.local_metadata.as_ref().map(|meta| meta.to_wire(None)), }, @@ -133,7 +133,7 @@ impl Connection { self.phase = ConnectionPhase::Established(EstablishedConnection { remote_peer_id: sender_id.clone(), remote_metadata: metadata.map(PeerMetadata::from_wire), - protocol_version: "1".to_string(), + protocol_version: PROTOCOL_VERSION.to_string(), established_at: now, document_subscriptions: HashMap::new(), }); @@ -172,7 +172,7 @@ impl Connection { ?target_id, "received Peer message from peer" ); - if selected_protocol_version != "1" { + if selected_protocol_version != PROTOCOL_VERSION { tracing::warn!(conn_id=?self.id, "peer does not support protocol version 1"); self.send( out, diff --git a/samod-core/src/actors/hub/state.rs b/samod-core/src/actors/hub/state.rs index 69bf5b2..f4c0ece 100644 --- a/samod-core/src/actors/hub/state.rs +++ b/samod-core/src/actors/hub/state.rs @@ -15,7 +15,7 @@ use crate::{ network::{ ConnDirection, ConnectionEvent, ConnectionInfo, ConnectionState, PeerDocState, PeerInfo, PeerMetadata, - wire_protocol::{WireMessage, WireMessageBuilder}, + wire_protocol::{PROTOCOL_VERSION, WireMessage, WireMessageBuilder}, }, }; @@ -579,7 +579,7 @@ impl State { let peer_info = PeerInfo { peer_id: remote_peer_id.clone(), metadata: Some(self.get_local_metadata()), - protocol_version: "1".to_string(), + protocol_version: PROTOCOL_VERSION.to_string(), }; out.emit_connection_event(ConnectionEvent::HandshakeCompleted { connection_id, diff --git a/samod-core/src/lib.rs b/samod-core/src/lib.rs index 2a8cf87..8baab0b 100644 --- a/samod-core/src/lib.rs +++ b/samod-core/src/lib.rs @@ -215,3 +215,6 @@ pub use unix_timestamp::UnixTimestamp; mod loader; pub use loader::{LoaderState, SamodLoader}; + +#[cfg(feature = "wasm")] +pub mod time_provider; diff --git a/samod-core/src/network/wire_protocol.rs b/samod-core/src/network/wire_protocol.rs index 6c57348..2179106 100644 --- a/samod-core/src/network/wire_protocol.rs +++ b/samod-core/src/network/wire_protocol.rs @@ -1,6 +1,8 @@ use crate::{DocumentId, PeerId, StorageId, actors::messages::SyncMessage}; use std::{collections::HashMap, str::FromStr}; +pub const PROTOCOL_VERSION: &str = "1"; + /// Metadata sent in join or peer messages #[derive(Debug, Clone, PartialEq, Eq)] pub struct PeerMetadata { @@ -734,7 +736,7 @@ mod tests { fn test_join_message_roundtrip() { let msg = WireMessage::Join { sender_id: PeerId::from("test-peer"), - supported_protocol_versions: vec!["1".to_string()], + supported_protocol_versions: vec![PROTOCOL_VERSION.to_string()], metadata: Some(PeerMetadata { storage_id: Some(StorageId::new(&mut rand::rng())), is_ephemeral: false, @@ -750,7 +752,7 @@ mod tests { fn test_peer_message_roundtrip() { let msg = WireMessage::Peer { sender_id: PeerId::from("sender"), - selected_protocol_version: "1".to_string(), + selected_protocol_version: PROTOCOL_VERSION.to_string(), target_id: PeerId::from("target"), metadata: None, }; diff --git a/samod-core/src/time_provider.rs b/samod-core/src/time_provider.rs new file mode 100644 index 0000000..c83100d --- /dev/null +++ b/samod-core/src/time_provider.rs @@ -0,0 +1,24 @@ +use wasm_bindgen::prelude::*; + +thread_local! { + static TIME_PROVIDER: std::cell::RefCell> = std::cell::RefCell::new(None); +} + +#[wasm_bindgen] +pub fn set_time_provider(callback: js_sys::Function) { + TIME_PROVIDER.with(|provider| { + *provider.borrow_mut() = Some(callback); + }); +} + +pub(crate) fn get_external_time() -> Option { + TIME_PROVIDER.with(|provider| { + let provider_ref = provider.borrow(); + if let Some(callback) = provider_ref.as_ref() { + let result = callback.call0(&JsValue::NULL).ok()?; + Some(result.as_f64()? as u128) + } else { + None + } + }) +} diff --git a/samod-core/src/unix_timestamp.rs b/samod-core/src/unix_timestamp.rs index 7782e77..089024e 100644 --- a/samod-core/src/unix_timestamp.rs +++ b/samod-core/src/unix_timestamp.rs @@ -1,8 +1,14 @@ use std::{ ops::{Add, AddAssign, Sub}, - time::{Duration, SystemTime, UNIX_EPOCH}, + time::Duration, }; +#[cfg(not(target_arch = "wasm32"))] +use std::time::{SystemTime, UNIX_EPOCH}; + +#[cfg(target_arch = "wasm32")] +use js_sys; + #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct UnixTimestamp { millis: u128, @@ -22,6 +28,40 @@ impl std::fmt::Debug for UnixTimestamp { impl UnixTimestamp { pub fn now() -> Self { + #[cfg(target_arch = "wasm32")] + { + // Try to get external time provider first + if let Some(millis) = crate::time_provider::get_external_time() { + return Self { millis }; + } + + let result = std::panic::catch_unwind(|| js_sys::Date::now()); + + match result { + Ok(millis) => Self { + millis: millis as u128, + }, + Err(_) => { + panic!( + "Cannot access Date.now() in this WASM environment!\n\ + \n\ + If you're using Node.js or another WASI runtime, you need to provide \n\ + a time function. Example for Node.js:\n\ + \n\ + ```javascript\n\ + import init, {{ set_time_provider }} from './samod_core.js';\n\ + \n\ + await init();\n\ + set_time_provider(() => Date.now());\n\ + ```\n\ + \n\ + See https://github.com/alexjg/samod#wasi-environments for more details." + ) + } + } + } + + #[cfg(not(target_arch = "wasm32"))] Self { millis: SystemTime::now() .duration_since(UNIX_EPOCH) diff --git a/samod-test-harness/Cargo.toml b/samod-test-harness/Cargo.toml index 09b6975..3e7952d 100644 --- a/samod-test-harness/Cargo.toml +++ b/samod-test-harness/Cargo.toml @@ -10,5 +10,4 @@ samod-core = { path = "../samod-core" } tracing = "0.1.41" [lib] - -pulish = false +publish = false diff --git a/samod/Cargo.toml b/samod/Cargo.toml index 6ba46db..04757ca 100644 --- a/samod/Cargo.toml +++ b/samod/Cargo.toml @@ -9,10 +9,11 @@ repository = "https://github.com/alexjg/samod" [features] tokio = ["dep:tokio", "dep:tokio-util"] -axum = ["dep:axum", "dep:tokio", "dep:tokio-util"] +axum = ["dep:axum", "tokio"] tungstenite = ["dep:tungstenite", "dep:tokio-tungstenite", "tokio"] gio = ["dep:gio", "dep:glib"] threadpool = ["dep:rayon"] +wasm = ["dep:wasm-bindgen", "dep:wasm-bindgen-futures", "dep:js-sys", "dep:web-sys", "samod-core/wasm"] [dependencies] automerge = "0.7.0" @@ -23,17 +24,54 @@ futures = "0.3.31" rand = "0.9.1" rayon = { version = "1.10.0", optional = true } samod-core = { path = "../samod-core", version = "0.5.0" } +tokio = { version = "1.46.0", features = ["rt", "time"], optional = true } +tracing = "0.1.41" +tungstenite = { version = "0.27.0", optional = true } +gio = { version = "0.21.1", optional = true } +glib = { version = "0.21.1", optional = true } +async-channel = "2.5.0" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +rayon = { version = "1.10.0", optional = true } +samod-core = { path = "../samod-core", version = "0.5.0" } tokio = { version = "1.46.0", features = ["rt", "time", "fs"], optional = true } tokio-tungstenite = { version = "0.27.0", optional = true } tokio-util = { version = "0.7.15", features = [ "codec", "net", ], optional = true } + +[target.'cfg(target_arch = "wasm32")'.dependencies] +wasm-bindgen = { version = "0.2", optional = true } +wasm-bindgen-futures = { version = "0.4", optional = true } +js-sys = { version = "0.3", optional = true } +web-sys = { version = "0.3", features = [ + "console", + "Window", + "WorkerGlobalScope", + "WebSocket", + "MessageEvent", + "CloseEvent", + "ErrorEvent", + "BinaryType", + "Blob", + "FileReader", + "File", + "FileSystemDirectoryHandle", + "FileSystemFileHandle", + "FileSystemWritableFileStream", +], optional = true } +indexed_db_futures = { version = "0.6.4", features = ["cursors", "async-upgrade"] } +console_error_panic_hook = "0.1" +tracing-wasm = "0.2" +tokio = { version = "1.46.0", features = ["rt", "time"], optional = true } +tokio-util = { version = "0.7.15", features = [ + "codec", +], optional = true } tracing = "0.1.41" tungstenite = { version = "0.27.0", optional = true } -gio = { version = "0.20.12", optional = true } -glib = { version = "0.20.12", optional = true } -async-channel = "2.5.0" +gio = { version = "0.21.1", optional = true } +glib = { version = "0.21.1", optional = true } [dev-dependencies] eyre = "0.6.12" @@ -42,17 +80,16 @@ reqwest = { version = "0.12.22", features = [ "blocking", ], default-features = false } tempfile = "3.20.0" -tokio = { version = "1.46.0", features = [ - "rt", - "time", - "macros", - "process", - "fs", -] } +tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } + +[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] +tokio = { version = "1.46.0", features = ["macros", "process"] } tokio-test = { version = "0.4.4" } tokio-stream = { version = "0.1.17", features = ["io-util"] } tokio-util = { version = "0.7.15", features = ["codec", "net"] } -tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } + +[lib] +crate-type = ["cdylib", "rlib"] [[test]] name = "js_interop" diff --git a/samod/interop-test-server/pnpm-lock.yaml b/samod/interop-test-server/pnpm-lock.yaml new file mode 100644 index 0000000..b4a0c2f --- /dev/null +++ b/samod/interop-test-server/pnpm-lock.yaml @@ -0,0 +1,1644 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@automerge/automerge': + specifier: ^2.2.9 + version: 2.2.9 + '@automerge/automerge-repo': + specifier: ^2.0.7 + version: 2.3.0 + '@automerge/automerge-repo-network-websocket': + specifier: ^2.0.7 + version: 2.3.0 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + cmd-ts: + specifier: ^0.13.0 + version: 0.13.0 + express: + specifier: ^5.1.0 + version: 5.1.0 + ws: + specifier: ^8.13.0 + version: 8.18.3 + devDependencies: + '@types/node': + specifier: ^18.14.0 + version: 18.19.124 + ts-mocha: + specifier: ^10.0.0 + version: 10.1.0(mocha@11.7.2) + ts-node: + specifier: ^10.9.1 + version: 10.9.2(@types/node@18.19.124)(typescript@4.9.5) + typescript: + specifier: ^4.9.5 + version: 4.9.5 + +packages: + + '@automerge/automerge-repo-network-websocket@2.3.0': + resolution: {integrity: sha512-1NddZXFu9Ry2Pfy7p/wbvG8TcsPvv87NPhD7+aiLb8miWrk1TjHYEUbwRQPMUN2lKAcDtnbpMZbJxbwgItxFWA==} + + '@automerge/automerge-repo@2.3.0': + resolution: {integrity: sha512-nZppJl90bK0zVGtByHDvXZ1gC8HCcKnhGkJxMLrPxuFlLdoAfq7x10Tk6AT0eMfx1048kl5zRDH7DpAZD+SvMw==} + + '@automerge/automerge@2.2.9': + resolution: {integrity: sha512-6HM52Ops79hAQBWMg/t0MNfGOdEiXyenQjO9F1hKZq0RWDsMLpPa1SzRy/C4/4UyX67sTHuA5CwBpH34SpfZlA==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.0': + resolution: {integrity: sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.0': + resolution: {integrity: sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.0': + resolution: {integrity: sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.0': + resolution: {integrity: sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.0': + resolution: {integrity: sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.0': + resolution: {integrity: sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==} + cpu: [x64] + os: [win32] + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + + '@types/json5@0.0.29': + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + + '@types/node@18.19.124': + resolution: {integrity: sha512-hY4YWZFLs3ku6D2Gqo3RchTd9VRCcrjqp/I0mmohYeUVA5Y8eCXKJEasHxLAJVZRJuQogfd1GiJ9lgogBgKeuQ==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + arrify@1.0.1: + resolution: {integrity: sha512-3CYzex9M9FGQjCGMGyi6/31c8GJbgb0qGyrx5HWxPd0aCwh4cB2YjMb2Xf9UuoogrMrlO9cTqnB5rI5GHZTcUA==} + engines: {node: '>=0.10.0'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base-x@4.0.1: + resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} + + body-parser@2.2.0: + resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} + engines: {node: '>=18'} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + + bs58@5.0.0: + resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + + bs58check@3.0.1: + resolution: {integrity: sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + cbor-extract@2.2.0: + resolution: {integrity: sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==} + hasBin: true + + cbor-x@1.6.0: + resolution: {integrity: sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + cmd-ts@0.13.0: + resolution: {integrity: sha512-nsnxf6wNIM/JAS7T/x/1JmbEsjH0a8tezXqqpaL0O6+eV0/aDEnRxwjxpu0VzDdRcaC1ixGSbRlUuf/IU59I4g==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + content-disposition@1.0.0: + resolution: {integrity: sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==} + engines: {node: '>= 0.6'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + + diff@3.5.0: + resolution: {integrity: sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==} + engines: {node: '>=0.3.1'} + + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + + diff@7.0.0: + resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} + engines: {node: '>=0.3.1'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + express@5.1.0: + resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} + engines: {node: '>= 18'} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + finalhandler@2.1.0: + resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} + engines: {node: '>= 0.8'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + glob@10.4.5: + resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + hasBin: true + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + + http-errors@2.0.0: + resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.0: + resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} + engines: {node: '>=0.10.0'} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + json5@1.0.2: + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} + hasBin: true + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@3.0.1: + resolution: {integrity: sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==} + engines: {node: '>= 0.6'} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mocha@11.7.2: + resolution: {integrity: sha512-lkqVJPmqqG/w5jmmFtiRvtA2jkDyNVUcefFJKb2uyX4dekk8Okgqop3cgbFiaIvj8uCRJVTP5x9dfxGyXm2jvQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.1: + resolution: {integrity: sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==} + engines: {node: '>= 0.10'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + send@1.2.0: + resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} + engines: {node: '>= 18'} + + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + + serve-static@2.2.0: + resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + statuses@2.0.1: + resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==} + engines: {node: '>= 0.8'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + strip-bom@3.0.0: + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + ts-mocha@10.1.0: + resolution: {integrity: sha512-T0C0Xm3/WqCuF2tpa0GNGESTBoKZaiqdUP8guNv4ZY316AFXlyidnrzQ1LUrCT0Wb1i3J0zFTgOh/55Un44WdA==} + engines: {node: '>= 6.X.X'} + hasBin: true + peerDependencies: + mocha: ^3.X.X || ^4.X.X || ^5.X.X || ^6.X.X || ^7.X.X || ^8.X.X || ^9.X.X || ^10.X.X || ^11.X.X + + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + + ts-node@7.0.1: + resolution: {integrity: sha512-BVwVbPJRspzNh2yfslyT1PSbl5uIk03EZlb493RKHN4qej/D06n1cEhjlOJG69oFsE7OT8XjpTUcYf6pKTLMhw==} + engines: {node: '>=4.2.0'} + hasBin: true + + tsconfig-paths@3.15.0: + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@4.9.5: + resolution: {integrity: sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==} + engines: {node: '>=4.2.0'} + hasBin: true + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + workerpool@9.3.4: + resolution: {integrity: sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg==} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.21.0: + resolution: {integrity: sha512-y4wmqxjyAa0tgz4k3m/MgTF1kDOahE5+xLfWt5eh1sk+43DatLhKlI8lQDJZpvihZavjbD3TUgy2PRMphhhqgQ==} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + + yn@2.0.0: + resolution: {integrity: sha512-uTv8J/wiWTgUTg+9vLTi//leUl5vDQS6uii/emeTb2ssY7vl6QWf2fFbIIGjnhjvbdKlU0ed7QPgY1htTC86jQ==} + engines: {node: '>=4'} + + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@automerge/automerge-repo-network-websocket@2.3.0': + dependencies: + '@automerge/automerge-repo': 2.3.0 + cbor-x: 1.6.0 + debug: 4.4.1(supports-color@8.1.1) + eventemitter3: 5.0.1 + isomorphic-ws: 5.0.0(ws@8.18.3) + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo@2.3.0': + dependencies: + '@automerge/automerge': 2.2.9 + bs58check: 3.0.1 + cbor-x: 1.6.0 + debug: 4.4.1(supports-color@8.1.1) + eventemitter3: 5.0.1 + fast-sha256: 1.3.0 + uuid: 9.0.1 + xstate: 5.21.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge@2.2.9': + dependencies: + uuid: 9.0.1 + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.0': + optional: true + + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.1.2 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@noble/hashes@1.8.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + + '@tsconfig/node10@1.0.11': {} + + '@tsconfig/node12@1.0.11': {} + + '@tsconfig/node14@1.0.3': {} + + '@tsconfig/node16@1.0.4': {} + + '@types/json5@0.0.29': + optional: true + + '@types/node@18.19.124': + dependencies: + undici-types: 5.26.5 + + '@types/ws@8.18.1': + dependencies: + '@types/node': 18.19.124 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.1 + negotiator: 1.0.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + arg@4.1.3: {} + + argparse@2.0.1: {} + + arrify@1.0.1: {} + + balanced-match@1.0.2: {} + + base-x@4.0.1: {} + + body-parser@2.2.0: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.1(supports-color@8.1.1) + http-errors: 2.0.0 + iconv-lite: 0.6.3 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.1 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + browser-stdout@1.3.1: {} + + bs58@5.0.0: + dependencies: + base-x: 4.0.1 + + bs58check@3.0.1: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 5.0.0 + + buffer-from@1.1.2: {} + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@6.3.0: {} + + cbor-extract@2.2.0: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.0 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.0 + '@cbor-extract/cbor-extract-linux-x64': 2.2.0 + '@cbor-extract/cbor-extract-win32-x64': 2.2.0 + optional: true + + cbor-x@1.6.0: + optionalDependencies: + cbor-extract: 2.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + cmd-ts@0.13.0: + dependencies: + chalk: 4.1.2 + debug: 4.4.1(supports-color@8.1.1) + didyoumean: 1.2.2 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - supports-color + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + content-disposition@1.0.0: + dependencies: + safe-buffer: 5.2.1 + + content-type@1.0.5: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + create-require@1.1.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.1(supports-color@8.1.1): + dependencies: + ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 + + decamelize@4.0.0: {} + + depd@2.0.0: {} + + detect-libc@2.0.4: + optional: true + + didyoumean@1.2.2: {} + + diff@3.5.0: {} + + diff@4.0.2: {} + + diff@7.0.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + eastasianwidth@0.2.0: {} + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + escalade@3.2.0: {} + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: {} + + etag@1.8.1: {} + + eventemitter3@5.0.1: {} + + express@5.1.0: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.0 + content-disposition: 1.0.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.1(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.0 + fresh: 2.0.0 + http-errors: 2.0.0 + merge-descriptors: 2.0.0 + mime-types: 3.0.1 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.0 + serve-static: 2.2.0 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-sha256@1.3.0: {} + + finalhandler@2.1.0: + dependencies: + debug: 4.4.1(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat@5.0.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + glob@10.4.5: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.5 + minipass: 7.1.2 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + he@1.2.0: {} + + http-errors@2.0.0: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.1 + toidentifier: 1.0.1 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.0: + dependencies: + safer-buffer: 2.1.2 + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-plain-obj@2.1.0: {} + + is-promise@4.0.0: {} + + is-unicode-supported@0.1.0: {} + + isexe@2.0.0: {} + + isomorphic-ws@5.0.0(ws@8.18.3): + dependencies: + ws: 8.18.3 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json5@1.0.2: + dependencies: + minimist: 1.2.8 + optional: true + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + + lru-cache@10.4.3: {} + + make-error@1.3.6: {} + + math-intrinsics@1.1.0: {} + + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.54.0: {} + + mime-types@3.0.1: + dependencies: + mime-db: 1.54.0 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minimist@1.2.8: {} + + minipass@7.1.2: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mocha@11.7.2: + dependencies: + browser-stdout: 1.3.1 + chokidar: 4.0.3 + debug: 4.4.1(supports-color@8.1.1) + diff: 7.0.0 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 10.4.5 + he: 1.2.0 + js-yaml: 4.1.0 + log-symbols: 4.1.0 + minimatch: 9.0.5 + ms: 2.1.3 + picocolors: 1.1.1 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 9.3.4 + yargs: 17.7.2 + yargs-parser: 21.1.1 + yargs-unparser: 2.0.0 + + ms@2.1.3: {} + + negotiator@1.0.0: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.0.4 + optional: true + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parseurl@1.3.3: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.2 + + path-to-regexp@8.3.0: {} + + picocolors@1.1.1: {} + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + + range-parser@1.2.1: {} + + raw-body@3.0.1: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.0 + iconv-lite: 0.7.0 + unpipe: 1.0.0 + + readdirp@4.1.2: {} + + require-directory@2.1.1: {} + + router@2.2.0: + dependencies: + debug: 4.4.1(supports-color@8.1.1) + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + send@1.2.0: + dependencies: + debug: 4.4.1(supports-color@8.1.1) + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.0 + mime-types: 3.0.1 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + + serve-static@2.2.0: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.0 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + signal-exit@4.1.0: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + statuses@2.0.1: {} + + statuses@2.0.2: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.1.2 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + strip-bom@3.0.0: + optional: true + + strip-json-comments@3.1.1: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-color@8.1.1: + dependencies: + has-flag: 4.0.0 + + toidentifier@1.0.1: {} + + ts-mocha@10.1.0(mocha@11.7.2): + dependencies: + mocha: 11.7.2 + ts-node: 7.0.1 + optionalDependencies: + tsconfig-paths: 3.15.0 + + ts-node@10.9.2(@types/node@18.19.124)(typescript@4.9.5): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.124 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 4.9.5 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + + ts-node@7.0.1: + dependencies: + arrify: 1.0.1 + buffer-from: 1.1.2 + diff: 3.5.0 + make-error: 1.3.6 + minimist: 1.2.8 + mkdirp: 0.5.6 + source-map-support: 0.5.21 + yn: 2.0.0 + + tsconfig-paths@3.15.0: + dependencies: + '@types/json5': 0.0.29 + json5: 1.0.2 + minimist: 1.2.8 + strip-bom: 3.0.0 + optional: true + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.1 + + typescript@4.9.5: {} + + undici-types@5.26.5: {} + + unpipe@1.0.0: {} + + uuid@9.0.1: {} + + v8-compile-cache-lib@3.0.1: {} + + vary@1.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + workerpool@9.3.4: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.1.2 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + xstate@5.21.0: {} + + y18n@5.0.8: {} + + yargs-parser@21.1.1: {} + + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yn@2.0.0: {} + + yn@3.1.1: {} + + yocto-queue@0.1.0: {} diff --git a/samod/interop-test-server/server.ts b/samod/interop-test-server/server.ts index 82ca214..27355c3 100644 --- a/samod/interop-test-server/server.ts +++ b/samod/interop-test-server/server.ts @@ -1,119 +1,224 @@ import express from "express"; import { WebSocketServer } from "ws"; import { - Chunk, - Repo, - RepoConfig, - StorageAdapterInterface, - StorageKey, + Chunk, + Repo, + RepoConfig, + StorageAdapterInterface, + StorageKey, } from "@automerge/automerge-repo"; import { NodeWSServerAdapter } from "@automerge/automerge-repo-network-websocket"; +import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); class Server { - #socket: WebSocketServer; - - #server: ReturnType; - #storage: InMemoryStorageAdapter; - - #repo: Repo; - - constructor(port: number) { - this.#socket = new WebSocketServer({ noServer: true }); - - const PORT = port; - const app = express(); - app.use(express.static("public")); - this.#storage = new InMemoryStorageAdapter(); - - const config: RepoConfig = { - // network: [new NodeWSServerAdapter(this.#socket) as any], - network: [new NodeWSServerAdapter(this.#socket as any)], - storage: this.#storage, - /** @ts-ignore @type {(import("automerge-repo").PeerId)} */ - peerId: `storage-server` as PeerId, - // Since this is a server, we don't share generously — meaning we only sync documents they already - // know about and can ask for by ID. - sharePolicy: async () => false, - }; - const serverRepo = new Repo(config); - this.#repo = serverRepo; - - app.get("/", (req, res) => { - res.send(`👍 @automerge/automerge-repo-sync-server is running`); - }); + #socket: WebSocketServer; + + #server: ReturnType; + #storage: InMemoryStorageAdapter; + + #repo: Repo; + #isWasmMode: boolean; + + constructor(port: number, options: { wasm?: boolean } = {}) { + this.#isWasmMode = options.wasm || false; + this.#socket = new WebSocketServer({ noServer: true }); + + const PORT = port; + const app = express(); + + // Serve static files for WASM mode + if (this.#isWasmMode) { + // Enable CORS for WASM testing + app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Headers', 'Content-Type'); + res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS'); + next(); + }); + + // Serve WASM test files + app.use('/wasm', express.static(path.join(__dirname, '../../wasm-tests/src'))); + app.use('/pkg', express.static(path.join(__dirname, '../../wasm-tests/pkg'))); + } + + app.use(express.static("public")); + this.#storage = new InMemoryStorageAdapter(); + + const config: RepoConfig = { + // network: [new NodeWSServerAdapter(this.#socket) as any], + network: [new NodeWSServerAdapter(this.#socket as any)], + storage: this.#storage, + /** @ts-ignore @type {(import("automerge-repo").PeerId)} */ + peerId: `storage-server` as PeerId, + // Since this is a server, we don't share generously — meaning we only sync documents they already + // know about and can ask for by ID. + sharePolicy: async () => true, + }; + const serverRepo = new Repo(config); + this.#repo = serverRepo; + + app.get("/", (req, res) => { + const mode = this.#isWasmMode ? ' (WASM mode)' : ''; + res.send(`👍 @automerge/automerge-repo-sync-server is running${mode}`); + }); - this.#server = app.listen(PORT, () => { - console.log(`Listening on port ${this.#server.address().port}`); - }); + // WASM-specific endpoints + if (this.#isWasmMode) { + app.get('/health', (req, res) => { + res.json({ + status: 'ok', + mode: 'wasm', + storage: this.#storage.size(), + connections: this.#socket.clients.size + }); + }); + + app.get('/stats', (req, res) => { + res.json({ + documentsStored: this.#storage.size(), + activeConnections: this.#socket.clients.size, + uptime: process.uptime() + }); + }); + + // Testing endpoints for disconnection + app.post('/test/disconnect-all', (req, res) => { + let disconnectedCount = 0; + for (const ws of this.#socket.clients) { + if (ws.readyState === ws.OPEN) { + ws.close(1000, 'Server-initiated disconnect for testing'); + disconnectedCount++; + } + } + res.json({ disconnected: disconnectedCount }); + }); + + app.post('/test/disconnect-random', (req, res) => { + const clients = Array.from(this.#socket.clients).filter(ws => ws.readyState === ws.OPEN); + if (clients.length === 0) { + res.json({ disconnected: 0, error: 'No active connections' }); + return; + } + + const randomClient = clients[Math.floor(Math.random() * clients.length)]; + randomClient.close(1000, 'Server-initiated disconnect for testing'); + res.json({ disconnected: 1 }); + }); + + // Clear all documents from server storage + app.post('/test/clear-storage', (req, res) => { + const previousSize = this.#storage.size(); + this.#storage.clear(); + res.json({ + previousSize, + currentSize: this.#storage.size(), + cleared: true + }); + }); + } + + this.#server = app.listen(PORT, () => { + const mode = this.#isWasmMode ? ' (WASM mode)' : ''; + console.log(`Listening on port ${this.#server.address().port}${mode}`); + }); - this.#server.on("upgrade", (request, socket, head) => { - console.log("upgrading to websocket"); - this.#socket.handleUpgrade(request, socket, head, (socket) => { - this.#socket.emit("connection", socket, request); - }); - }); - } + this.#server.on("upgrade", (request, socket, head) => { + console.log(`Upgrading to websocket${this.#isWasmMode ? ' (WASM client)' : ''}`); + this.#socket.handleUpgrade(request, socket, head, (socket) => { + this.#socket.emit("connection", socket, request); + }); + }); + } - close() { - this.#storage.log(); - this.#socket.close(); - this.#server.close(); - } + close() { + this.#storage.log(); + this.#socket.close(); + this.#server.close(); + } } class InMemoryStorageAdapter implements StorageAdapterInterface { - #data: Map = new Map(); - - async load(key: StorageKey): Promise { - return this.#data.get(key); - } - async save(key: StorageKey, data: Uint8Array): Promise { - this.#data.set(key, data); - } - async remove(key: StorageKey): Promise { - this.#data.delete(key); - } - async loadRange(keyPrefix: StorageKey): Promise { - let result: Chunk[] = []; - for (const [key, value] of this.#data.entries()) { - if (isPrefixOf(keyPrefix, key)) { - result.push({ - key, - data: value, - }); - } - } - return result; - } + #data: Map = new Map(); - removeRange(keyPrefix: StorageKey): Promise { - for (const [key] of this.#data.entries()) { - if (isPrefixOf(keyPrefix, key)) { + async load(key: StorageKey): Promise { + return this.#data.get(key); + } + async save(key: StorageKey, data: Uint8Array): Promise { + this.#data.set(key, data); + } + async remove(key: StorageKey): Promise { this.#data.delete(key); - } } - return Promise.resolve(); - } + async loadRange(keyPrefix: StorageKey): Promise { + let result: Chunk[] = []; + for (const [key, value] of this.#data.entries()) { + if (isPrefixOf(keyPrefix, key)) { + result.push({ + key, + data: value, + }); + } + } + return result; + } - log() { - console.log(`InMemoryStorageAdapter has ${this.#data.size} items:`); - for (const [key, value] of this.#data.entries()) { - console.log(` ${key.join("/")}: ${value.length} bytes`); + removeRange(keyPrefix: StorageKey): Promise { + for (const [key] of this.#data.entries()) { + if (isPrefixOf(keyPrefix, key)) { + this.#data.delete(key); + } + } + return Promise.resolve(); + } + + log() { + console.log(`InMemoryStorageAdapter has ${this.#data.size} items:`); + for (const [key, value] of this.#data.entries()) { + console.log(` ${key.join("/")}: ${value.length} bytes`); + } + } + + size(): number { + return this.#data.size; + } + + clear(): void { + this.#data.clear(); } - } } function isPrefixOf(prefix: StorageKey, candidate: StorageKey): boolean { - return ( - prefix.length <= candidate.length && - prefix.every((segment, index) => segment === candidate[index]) - ); + return ( + prefix.length <= candidate.length && + prefix.every((segment, index) => segment === candidate[index]) + ); } -const port = process.argv[2] ? parseInt(process.argv[2]) : 8080; -const server = new Server(port); +export { Server }; + +// Only start server if run directly +if (import.meta.url === `file://${process.argv[1]}`) { + // Parse command line arguments + const args = process.argv.slice(2); + const port = args.find(arg => !isNaN(parseInt(arg))) ? parseInt(args.find(arg => !isNaN(parseInt(arg)))!) : 3001; + const isWasmMode = args.includes('--wasm'); -process.on("SIGINT", () => { - server.close(); - process.exit(0); -}); + if (isWasmMode) { + console.log('Starting server in WASM test mode...'); + } + + const server = new Server(port, { wasm: isWasmMode }); + + process.on("SIGINT", () => { + server.close(); + process.exit(0); + }); + + process.on("SIGTERM", () => { + server.close(); + process.exit(0); + }); +} diff --git a/samod/src/conn_finished_reason.rs b/samod/src/conn_finished_reason.rs index b10a9ca..2858f84 100644 --- a/samod/src/conn_finished_reason.rs +++ b/samod/src/conn_finished_reason.rs @@ -11,4 +11,6 @@ pub enum ConnFinishedReason { ErrorReceiving(String), /// There was some error on the network transport when sending data ErrorSending(String), + /// There was some error configuring a connection + Error(String), } diff --git a/samod/src/lib.rs b/samod/src/lib.rs index ed2f982..ff1884c 100644 --- a/samod/src/lib.rs +++ b/samod/src/lib.rs @@ -349,6 +349,9 @@ pub mod runtime; mod unbounded; pub mod websocket; +#[cfg(feature = "wasm")] +pub mod wasm; + /// The entry point to this library /// /// A [`Repo`] represents a set of running [`DocHandle`]s, active connections to @@ -379,8 +382,8 @@ pub struct Repo { } impl Repo { - // Create a new [`RepoBuilder`] which will build a [`Repo`] that spawns its - // tasks onto the provided runtime + /// Create a new [`RepoBuilder`] which will build a [`Repo`] that spawns its + /// tasks onto the provided runtime pub fn builder( runtime: R, ) -> RepoBuilder { @@ -397,14 +400,22 @@ impl Repo { builder::RepoBuilder::new(::tokio::runtime::Handle::current()) } - // Create a new [`RepoBuilder`] which will build a [`Repo`] that spawns it's - // tasks onto a [`futures::executor::LocalPool`] + /// Create a new [`RepoBuilder`] which will build a [`Repo`] that spawns it's + /// tasks onto a [`futures::executor::LocalPool`] pub fn build_localpool( spawner: futures::executor::LocalSpawner, ) -> RepoBuilder { builder::RepoBuilder::new(spawner) } + /// Create a new [`RepoBuilder`] which will build a [`Repo`] that spawns it's + /// tasks using wasm-bindgen-futures for WASM environments + #[cfg(feature = "wasm")] + pub fn build_wasm() + -> RepoBuilder { + builder::RepoBuilder::new(crate::runtime::wasm::WasmRuntime::new()) + } + /// Create a new [`Repo`] instance which will build a [`Repo`] that spawns /// its tasks onto the current gio mainloop /// @@ -603,14 +614,18 @@ impl Repo { where SendErr: std::error::Error + Send + Sync + 'static, RecvErr: std::error::Error + Send + Sync + 'static, - Snk: Sink, Error = SendErr> + Send + 'static + Unpin, - Str: Stream, RecvErr>> + Send + 'static + Unpin, + Snk: Sink, Error = SendErr> + 'static + Unpin, + Str: Stream, RecvErr>> + 'static + Unpin, { - tracing::Span::current().record( - "local_peer_id", - self.inner.lock().unwrap().hub.peer_id().to_string(), + let local_peer_id = self.inner.lock().unwrap().hub.peer_id(); + tracing::info!( + "Creating connection with direction {:?}, local peer: {}", + direction, + local_peer_id ); + tracing::Span::current().record("local_peer_id", local_peer_id.to_string()); let DispatchedCommand { command_id, event } = HubEvent::create_connection(direction); + tracing::debug!("Created connection command_id: {:?}", command_id); let (tx, rx) = oneshot::channel(); self.inner .lock() @@ -618,6 +633,7 @@ impl Repo { .pending_commands .insert(command_id, tx); self.inner.lock().unwrap().handle_event(event); + tracing::debug!("Handled create_connection event"); let inner = self.inner.clone(); async move { @@ -639,12 +655,17 @@ impl Repo { }; let mut stream = stream.fuse(); + tracing::info!( + "Starting connection message loop for connection {:?}", + connection_id + ); let result = loop { futures::select! { next_inbound_msg = stream.next() => { if let Some(msg) = next_inbound_msg { match msg { Ok(msg) => { + tracing::debug!("Received {} bytes from stream", msg.len()); let DispatchedCommand { event, .. } = HubEvent::receive(connection_id, msg); inner.lock().unwrap().handle_event(event); } @@ -660,6 +681,7 @@ impl Repo { }, next_outbound = rx.next() => { if let Some(next_outbound) = next_outbound { + tracing::debug!("Sending {} bytes to stream", next_outbound.len()); if let Err(e) = sink.send(next_outbound).await { tracing::error!(err=?e, "error sending, closing connection"); break ConnFinishedReason::ErrorSending(e.to_string()); @@ -671,6 +693,7 @@ impl Repo { } } }; + tracing::info!("Connection message loop ended with result: {:?}", result); if !(result == ConnFinishedReason::WeDisconnected) { let event = HubEvent::connection_lost(connection_id); inner.lock().unwrap().handle_event(event); @@ -682,6 +705,160 @@ impl Repo { } } + #[cfg(feature = "wasm")] + /// Connect with an additional ready signal for controlled connection + pub fn connect_with_ready_signal( + &self, + stream: Str, + sink: Snk, + direction: ConnDirection, + ready_signal: oneshot::Sender<()>, + ) -> impl Future + 'static + where + SendErr: std::error::Error + Send + Sync + 'static, + RecvErr: std::error::Error + Send + Sync + 'static, + Snk: Sink, Error = SendErr> + 'static + Unpin, + Str: Stream, RecvErr>> + 'static + Unpin, + { + use std::sync::Arc; + use std::sync::atomic::{AtomicBool, Ordering}; + + let ready_sent = Arc::new(AtomicBool::new(false)); + let ready_sent_clone = Arc::clone(&ready_sent); + let mut ready_signal = Some(ready_signal); + + let monitoring_sink = sink.with(move |msg| { + if !ready_sent_clone.load(Ordering::Relaxed) { + ready_sent_clone.store(true, Ordering::Relaxed); + if let Some(signal) = ready_signal.take() { + let _ = signal.send(()); + } + tracing::info!("WASM: Protocol handshake complete (first message sent)"); + } + futures::future::ready(Ok::, SendErr>(msg)) + }); + + self.connect(stream, monitoring_sink, direction) + } + + /// Connect with an additional close signal for controlled disconnection + pub async fn connect_with_close_signal( + &self, + stream: Str, + mut sink: Snk, + direction: ConnDirection, + close_signal: CloseFuture, + ) -> ConnFinishedReason + where + SendErr: std::error::Error + Send + Sync + 'static, + RecvErr: std::error::Error + Send + Sync + 'static, + Snk: Sink, Error = SendErr> + Send + 'static + Unpin, + Str: Stream, RecvErr>> + Send + 'static + Unpin, + CloseFuture: Future> + Unpin, + { + tracing::Span::current().record( + "local_peer_id", + self.inner.lock().unwrap().hub.peer_id().to_string(), + ); + let DispatchedCommand { command_id, event } = HubEvent::create_connection(direction); + let (tx, rx) = oneshot::channel(); + self.inner + .lock() + .unwrap() + .pending_commands + .insert(command_id, tx); + self.inner.lock().unwrap().handle_event(event); + + let inner = self.inner.clone(); + + let connection_id = match rx.await { + Ok(CommandResult::CreateConnection { connection_id }) => connection_id, + Ok(other) => panic!("unexpected command result for create connection: {other:?}"), + Err(_) => return ConnFinishedReason::Shutdown, + }; + + let mut rx = { + let mut rx = inner + .lock() + .unwrap() + .connections + .get_mut(&connection_id) + .map(|ConnHandle { rx, .. }| rx.take()) + .expect("connection not found"); + rx.take().expect("receive end not found") + }; + + let mut stream = stream.fuse(); + let mut close_signal = close_signal.fuse(); + + tracing::info!( + "Starting connection message loop for connection {:?}", + connection_id + ); + + #[cfg(target_arch = "wasm32")] + web_sys::console::log_1( + &format!( + "WASM: Starting connection message loop for connection {:?}", + connection_id + ) + .into(), + ); + + let result = loop { + futures::select! { + _ = close_signal => { + tracing::debug!("close signal received, closing connection"); + break ConnFinishedReason::WeDisconnected; + }, + next_inbound_msg = stream.next() => { + if let Some(msg) = next_inbound_msg { + match msg { + Ok(msg) => { + tracing::debug!("Received {} bytes from stream", msg.len()); + #[cfg(target_arch = "wasm32")] + web_sys::console::log_1(&format!("WASM: Received {} bytes from stream", msg.len()).into()); + let DispatchedCommand { event, .. } = HubEvent::receive(connection_id, msg); + inner.lock().unwrap().handle_event(event); + } + Err(e) => { + tracing::error!(err=?e, "error receiving, closing connection"); + break ConnFinishedReason::ErrorReceiving(e.to_string()); + } + } + } else { + tracing::debug!("stream closed, closing connection"); + break ConnFinishedReason::TheyDisconnected; + } + }, + next_outbound = rx.next() => { + if let Some(next_outbound) = next_outbound { + tracing::debug!("Sending {} bytes to stream", next_outbound.len()); + #[cfg(target_arch = "wasm32")] + web_sys::console::log_1(&format!("WASM: Sending {} bytes to stream", next_outbound.len()).into()); + if let Err(e) = sink.send(next_outbound).await { + tracing::error!(err=?e, "error sending, closing connection"); + break ConnFinishedReason::ErrorSending(e.to_string()); + } + } else { + tracing::debug!(?connection_id, "connection closing"); + #[cfg(target_arch = "wasm32")] + web_sys::console::log_1(&format!("WASM: Connection {:?} closing", connection_id).into()); + break ConnFinishedReason::WeDisconnected; + } + } + } + }; + if !(result == ConnFinishedReason::WeDisconnected) { + let event = HubEvent::connection_lost(connection_id); + inner.lock().unwrap().handle_event(event); + } + if let Err(e) = sink.close().await { + tracing::error!(err=?e, "error closing sink"); + } + result + } + /// Wait for some connection to be established with the given remote peer ID /// /// This will resolve immediately if the peer is already connected, otherwise @@ -715,6 +892,23 @@ impl Repo { self.inner.lock().unwrap().hub.peer_id().clone() } + /// List all document IDs that are stored locally + /// + /// This method returns document IDs of all currently active document handles. + /// For now, this implementation returns the document IDs of documents that are + /// currently loaded in memory. A future enhancement could scan storage for + /// persisted documents, but this would require significant changes to the + /// architecture to access storage from the Repo level. + pub fn list_documents(&self) -> Result, Stopped> { + let inner = self.inner.lock().unwrap(); + let doc_ids: Vec = inner + .actors + .values() + .map(|actor_handle| actor_handle.doc.document_id().clone()) + .collect(); + Ok(doc_ids) + } + /// Stop the `Samod` instance. /// /// This will wait until all storage tasks have completed before stopping all diff --git a/samod/src/runtime.rs b/samod/src/runtime.rs index 2782763..fde4222 100644 --- a/samod/src/runtime.rs +++ b/samod/src/runtime.rs @@ -7,6 +7,8 @@ pub mod gio; pub mod localpool; #[cfg(feature = "tokio")] mod tokio; +#[cfg(target_arch = "wasm32")] +pub mod wasm; /// An abstraction over the asynchronous runtime the repo is running on /// diff --git a/samod/src/runtime/wasm.rs b/samod/src/runtime/wasm.rs new file mode 100644 index 0000000..0dedab4 --- /dev/null +++ b/samod/src/runtime/wasm.rs @@ -0,0 +1,30 @@ +use std::pin::Pin; +use std::future::Future; + +use crate::runtime::{LocalRuntimeHandle, RuntimeHandle}; + +pub struct WasmRuntime; + +impl WasmRuntime { + pub fn new() -> Self { + Self + } +} + +impl Default for WasmRuntime { + fn default() -> Self { + Self::new() + } +} + +impl RuntimeHandle for WasmRuntime { + fn spawn(&self, f: Pin + Send + 'static>>) { + wasm_bindgen_futures::spawn_local(f); + } +} + +impl LocalRuntimeHandle for WasmRuntime { + fn spawn(&self, f: Pin>>) { + wasm_bindgen_futures::spawn_local(f); + } +} diff --git a/samod/src/storage.rs b/samod/src/storage.rs index c0c60f7..30c16b7 100644 --- a/samod/src/storage.rs +++ b/samod/src/storage.rs @@ -4,14 +4,21 @@ use std::collections::HashMap; pub use samod_core::StorageKey; +#[cfg(not(target_arch = "wasm32"))] mod filesystem; mod in_memory; +#[cfg(target_arch = "wasm32")] +mod indexeddb; + pub use in_memory::InMemoryStorage; -#[cfg(feature = "tokio")] +#[cfg(target_arch = "wasm32")] +pub use indexeddb::IndexedDbStorage; + +#[cfg(all(feature = "tokio", not(target_arch = "wasm32")))] pub use filesystem::tokio::FilesystemStorage as TokioFilesystemStorage; -#[cfg(feature = "gio")] +#[cfg(all(feature = "gio", not(target_arch = "wasm32")))] pub use filesystem::gio::FilesystemStorage as GioFilesystemStorage; /// The storage abstraction used by a [`Repo`](crate::Repo) to store document data diff --git a/samod/src/storage/indexeddb.rs b/samod/src/storage/indexeddb.rs new file mode 100644 index 0000000..5b6e767 --- /dev/null +++ b/samod/src/storage/indexeddb.rs @@ -0,0 +1,281 @@ +use std::{collections::HashMap, future::Future}; + +use indexed_db_futures::{database::Database, prelude::*, transaction::TransactionMode}; +use samod_core::StorageKey; +use wasm_bindgen::JsValue; + +use crate::storage::LocalStorage; + +/// A [`LocalStorage`] implementation for browser WASM which stores data in IndexedDB +#[derive(Clone)] +pub struct IndexedDbStorage { + db_name: String, + store_name: String, +} + +impl IndexedDbStorage { + pub fn new() -> Self { + Self::with_names("samod_storage", "data") + } + + pub fn with_names(db_name: &str, store_name: &str) -> Self { + Self { + db_name: db_name.to_string(), + store_name: store_name.to_string(), + } + } + + async fn open_db(&self) -> Result { + let store_name = self.store_name.clone(); + Database::open(&self.db_name) + .with_version(1u32) + .with_on_upgrade_needed(move |_event, db| { + // Create object store if it doesn't exist + if !db.object_store_names().any(|name| name == store_name) { + let _ = db.create_object_store(&store_name).build(); + } + Ok(()) + }) + .await + .map_err(|_| JsValue::from_str("Failed to open IndexedDB connection")) + } + + fn storage_key_to_js_value(key: &StorageKey) -> JsValue { + JsValue::from_str(&key.to_string()) + } + + fn js_value_to_storage_key(value: JsValue) -> Result { + let key_parts: Vec = value + .as_string() + .ok_or_else(|| JsValue::from_str("Invalid key format"))? + .split("/") + .map(|s| s.to_string()) + .collect(); + StorageKey::from_parts(key_parts) + .map_err(|_| JsValue::from_str("Failed to create StorageKey")) + } +} + +impl Default for IndexedDbStorage { + fn default() -> Self { + Self::new() + } +} + +impl LocalStorage for IndexedDbStorage { + #[tracing::instrument(skip(self), level = "trace", ret)] + fn load(&self, key: StorageKey) -> impl Future>> { + let self_clone = self.clone(); + async move { + match self_clone.open_db().await { + Ok(db) => { + let tx = match db + .transaction(&self_clone.store_name) + .with_mode(TransactionMode::Readonly) + .build() + { + Ok(tx) => tx, + Err(e) => { + tracing::error!("Failed to create transaction: {:?}", e); + return None; + } + }; + + let store = match tx.object_store(&self_clone.store_name) { + Ok(store) => store, + Err(e) => { + tracing::error!("Failed to get object store: {:?}", e); + return None; + } + }; + + let js_key = Self::storage_key_to_js_value(&key); + + match store.get(js_key).primitive() { + Ok(request) => match request.await { + Ok(Some(result)) => { + let uint8_array = js_sys::Uint8Array::new(&result); + Some(uint8_array.to_vec()) + } + Ok(None) => None, + Err(_) => { + tracing::warn!("Failed to load from IndexedDB"); + None + } + }, + Err(e) => { + tracing::error!("Failed to create get request: {:?}", e); + None + } + } + } + Err(e) => { + tracing::error!("Failed to open database: {:?}", e); + None + } + } + } + } + + #[tracing::instrument(skip(self), level = "trace", ret)] + fn load_range( + &self, + prefix: StorageKey, + ) -> impl Future>> { + let self_clone = self.clone(); + async move { + let mut result = HashMap::new(); + + match self_clone.open_db().await { + Ok(db) => { + let tx = match db + .transaction(&self_clone.store_name) + .with_mode(TransactionMode::Readonly) + .build() + { + Ok(tx) => tx, + Err(e) => { + tracing::error!("Failed to create transaction: {:?}", e); + return HashMap::new(); + } + }; + + let store = match tx.object_store(&self_clone.store_name) { + Ok(store) => store, + Err(e) => { + tracing::error!("Failed to get object store: {:?}", e); + return HashMap::new(); + } + }; + + match store.open_cursor().await { + Ok(Some(mut cursor)) => loop { + match cursor.next_record::().await { + Ok(Some(js_value)) => { + if let Ok(Some(js_key)) = cursor.key::() { + if let Ok(storage_key) = + Self::js_value_to_storage_key(js_key) + { + if prefix.is_prefix_of(&storage_key) { + let uint8_array = + js_sys::Uint8Array::new(&js_value); + result.insert(storage_key, uint8_array.to_vec()); + } + } + } + } + Ok(None) => break, // end of cursor + Err(_) => break, + } + }, + Ok(None) => {} + Err(e) => { + tracing::error!("Failed to open cursor: {:?}", e); + } + } + } + Err(e) => { + tracing::error!("Failed to open database: {:?}", e); + } + } + + result + } + } + + #[tracing::instrument(skip(self, data), level = "trace")] + fn put(&self, key: StorageKey, data: Vec) -> impl Future { + let self_clone = self.clone(); + async move { + match self_clone.open_db().await { + Ok(db) => { + let tx = match db + .transaction(&self_clone.store_name) + .with_mode(TransactionMode::Readwrite) + .build() + { + Ok(tx) => tx, + Err(e) => { + tracing::error!("Failed to create transaction: {:?}", e); + return; + } + }; + + let store = match tx.object_store(&self_clone.store_name) { + Ok(store) => store, + Err(e) => { + tracing::error!("Failed to get object store: {:?}", e); + return; + } + }; + + let js_key = Self::storage_key_to_js_value(&key); + let uint8_array = js_sys::Uint8Array::from(&data[..]); + + let request = store.put(uint8_array.to_vec()).with_key(js_key); + match request.await { + Ok(_) => { + // Commit the transaction + if let Err(e) = tx.commit().await { + tracing::error!("Failed to commit transaction: {:?}", e); + } + } + Err(e) => { + tracing::error!("Failed to put data to IndexedDB: {:?}", e); + } + } + } + Err(e) => { + tracing::error!("Failed to open database: {:?}", e); + } + } + } + } + + #[tracing::instrument(skip(self), level = "trace")] + fn delete(&self, key: StorageKey) -> impl Future { + let self_clone = self.clone(); + async move { + match self_clone.open_db().await { + Ok(db) => { + let tx = match db + .transaction(&self_clone.store_name) + .with_mode(TransactionMode::Readwrite) + .build() + { + Ok(tx) => tx, + Err(e) => { + tracing::error!("Failed to create transaction: {:?}", e); + return; + } + }; + + let store = match tx.object_store(&self_clone.store_name) { + Ok(store) => store, + Err(e) => { + tracing::error!("Failed to get object store: {:?}", e); + return; + } + }; + + let js_key = Self::storage_key_to_js_value(&key); + + match store.delete(js_key).await { + Ok(_) => { + // Commit the transaction + if let Err(e) = tx.commit().await { + tracing::error!("Failed to commit transaction: {:?}", e); + } + } + Err(e) => { + tracing::error!("Failed to delete from IndexedDB: {:?}", e); + } + } + } + Err(e) => { + tracing::error!("Failed to open database: {:?}", e); + } + } + } + } +} \ No newline at end of file diff --git a/samod/src/wasm.rs b/samod/src/wasm.rs new file mode 100644 index 0000000..3aad281 --- /dev/null +++ b/samod/src/wasm.rs @@ -0,0 +1,654 @@ +//! WASM bindings for Samod +//! +//! This module provides JavaScript-friendly bindings for using Samod in web browsers. +//! All the main functionality of the Rust API is exposed through WebAssembly bindings. + +use std::str::FromStr; + +use automerge::{Automerge, transaction::Transactable}; +use futures::{StreamExt, channel::oneshot}; +use js_sys::Promise; +use samod_core::DocumentId; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +use crate::{ConnDirection, ConnFinishedReason, DocHandle, Repo, storage::IndexedDbStorage}; + +/// JavaScript error type for WASM bindings +#[wasm_bindgen] +pub struct WasmError { + message: String, +} + +#[wasm_bindgen] +impl WasmError { + #[wasm_bindgen(getter)] + pub fn message(&self) -> String { + self.message.clone() + } +} + +impl From for WasmError { + fn from(message: String) -> Self { + Self { message } + } +} + +impl From<&str> for WasmError { + fn from(message: &str) -> Self { + Self { + message: message.to_string(), + } + } +} + +impl From for WasmError { + fn from(error: automerge::AutomergeError) -> Self { + Self { + message: format!("Automerge error: {}", error), + } + } +} + +/// WASM wrapper around a Samod repository +#[wasm_bindgen] +pub struct WasmRepo { + inner: Repo, +} + +#[wasm_bindgen] +impl WasmRepo { + /// Create a new WASM repository with IndexedDB storage + #[wasm_bindgen(constructor)] + pub fn new() -> Promise { + // Initialize WASM tracing to forward Rust logs to browser console + console_error_panic_hook::set_once(); + tracing_wasm::set_as_global_default(); + + future_to_promise(async { + tracing::info!("WASM: Initializing Samod repository with IndexedDB storage"); + + // Always use fixed database and store names for consistency + let storage = IndexedDbStorage::with_names("samod_db", "documents"); + let repo = Repo::build_wasm() + .with_storage(storage.clone()) + .load_local() + .await; + + // Create a WasmRepo instance + let wasm_repo = WasmRepo { inner: repo }; + + // Restore documents from storage + if let Err(e) = wasm_repo.restore_documents_from_storage(storage).await { + tracing::warn!("Failed to restore some documents from storage: {}", e); + } + + tracing::info!("WASM: Repository initialized and documents restored"); + Ok(JsValue::from(wasm_repo)) + }) + } + + /// Create a new document with optional initial content + #[wasm_bindgen(js_name = "createDocument")] + pub fn create_document(&self, initial_content: JsValue) -> Promise { + let repo = self.inner.clone(); + + future_to_promise(async move { + let initial_doc = if !initial_content.is_undefined() { + let mut doc = Automerge::new(); + populate_doc_from_js_value(&mut doc, &initial_content).map_err(|e| { + WasmError::from(format!( + "Failed to populate document from initial content: {}", + e + )) + })?; + doc + } else { + Automerge::new() + }; + + match repo.create(initial_doc).await { + Ok(doc_handle) => Ok(JsValue::from(WasmDocHandle::new(doc_handle))), + Err(_) => Err(JsValue::from(WasmError::from("Failed to create document"))), + } + }) + } + + /// Find an existing document by ID + #[wasm_bindgen(js_name = "findDocument")] + pub fn find_document(&self, document_id: &str) -> Promise { + let repo = self.inner.clone(); + let doc_id = document_id.to_string(); + + future_to_promise(async move { + let document_id = match DocumentId::from_str(&doc_id) { + Ok(id) => id, + Err(_) => return Err(JsValue::from(WasmError::from("Invalid document ID format"))), + }; + + match repo.find(document_id).await { + Ok(Some(doc_handle)) => Ok(JsValue::from(WasmDocHandle::new(doc_handle))), + Ok(None) => Ok(JsValue::NULL), + Err(_) => Err(JsValue::from(WasmError::from("Failed to find document"))), + } + }) + } + + /// Get the peer ID of this repository + #[wasm_bindgen(js_name = "peerId")] + pub fn peer_id(&self) -> String { + self.inner.peer_id().to_string() + } + + /// Stop the repository and flush all data to storage + #[wasm_bindgen] + pub fn stop(&self) -> Promise { + let repo = self.inner.clone(); + + future_to_promise(async move { + repo.stop().await; + Ok(JsValue::UNDEFINED) + }) + } + + /// List all document IDs that are stored locally + #[wasm_bindgen(js_name = "listDocuments")] + pub fn list_documents(&self) -> Result { + match self.inner.list_documents() { + Ok(doc_ids) => { + let js_array = js_sys::Array::new(); + for doc_id in doc_ids { + js_array.push(&JsValue::from_str(&doc_id.to_string())); + } + Ok(js_array) + } + Err(_) => Err(WasmError::from("Failed to list documents")), + } + } + + /// Connect to a WebSocket server for real-time synchronization + /// Returns a handle that can be used to close the connection + #[wasm_bindgen(js_name = "connectWebSocket")] + pub fn connect_websocket(&self, url: &str) -> Result { + #[cfg(feature = "wasm")] + { + let repo = self.inner.clone(); + let url = url.to_string(); + + web_sys::console::log_1( + &format!("WASM: Creating WebSocket connection handle for {}", url).into(), + ); + + // Create channels to signal close and receive finish notification + let (close_tx, close_rx) = oneshot::channel::<()>(); + let (finish_tx, finish_rx) = oneshot::channel::(); + + // Spawn the connection task + wasm_bindgen_futures::spawn_local(async move { + web_sys::console::log_1( + &format!("WASM: Starting connection task for {}", url).into(), + ); + + // Create the websocket connection + match crate::websocket::WasmWebSocket::connect(&url).await { + Ok(ws) => { + web_sys::console::log_1( + &"WASM: WebSocket connected, splitting into sink/stream".into(), + ); + let (sink, stream) = StreamExt::split(ws); + + web_sys::console::log_1(&"WASM: About to call connect_with_close_signal - protocol handshake should start".into()); + + // Run the connection with close signal + let result = repo + .connect_with_close_signal( + stream, + sink, + ConnDirection::Outgoing, + close_rx, + ) + .await; + + web_sys::console::log_1( + &format!("WASM: Connection ended with: {:?}", result).into(), + ); + let reason_str = match result { + ConnFinishedReason::WeDisconnected => { + web_sys::console::log_1( + &"WASM: Normal close initiated by client".into(), + ); + "we_disconnected" + } + ConnFinishedReason::TheyDisconnected => { + web_sys::console::log_1( + &"WASM: Connection closed by server".into(), + ); + "server_disconnected" + } + ConnFinishedReason::ErrorReceiving(_) => { + web_sys::console::log_1( + &"WASM: Connection ended due to receive error".into(), + ); + "error_receiving" + } + ConnFinishedReason::ErrorSending(_) => { + web_sys::console::log_1( + &"WASM: Connection ended due to send error".into(), + ); + "error_sending" + } + ConnFinishedReason::Error(_) => { + web_sys::console::log_1( + &"WASM: Connection ended due to generic error".into(), + ); + "error" + } + ConnFinishedReason::Shutdown => { + web_sys::console::log_1( + &"WASM: Connection ended due to shutdown".into(), + ); + "shutdown" + } + }; + + // Notify about the connection ending + let _ = finish_tx.send(reason_str.to_string()); + } + Err(e) => { + web_sys::console::error_1( + &format!("WASM: WebSocket connection failed: {}", e).into(), + ); + // Notify about the connection failure + let _ = finish_tx.send("connection_failed".to_string()); + } + } + }); + + web_sys::console::log_1(&"WASM: Connection task spawned, returning handle".into()); + + Ok(WasmWebSocketHandle { + close_sender: Some(close_tx), + finish_receiver: Some(finish_rx), + }) + } + } + + /// Connect to a WebSocket server and wait for completion (legacy API) + #[wasm_bindgen(js_name = "connectWebSocketAsync")] + pub fn connect_websocket_async(&self, url: &str) -> Promise { + let repo = self.inner.clone(); + let url = url.to_string(); + + future_to_promise(async move { + #[cfg(feature = "wasm")] + { + let result = repo + .connect_wasm_websocket(&url, ConnDirection::Outgoing) + .await; + match result { + ConnFinishedReason::Shutdown => Ok(JsValue::from_str("shutdown")), + ConnFinishedReason::TheyDisconnected => { + Ok(JsValue::from_str("they_disconnected")) + } + ConnFinishedReason::WeDisconnected => Ok(JsValue::from_str("we_disconnected")), + ConnFinishedReason::ErrorReceiving(err) => Err(JsValue::from(WasmError::from( + format!("Error receiving: {}", err), + ))), + ConnFinishedReason::ErrorSending(err) => Err(JsValue::from(WasmError::from( + format!("Error sending: {}", err), + ))), + ConnFinishedReason::Error(err) => Err(JsValue::from(WasmError::from(err))), + } + } + }) + } +} + +impl WasmRepo { + /// Restore documents from IndexedDB storage + /// This scans the storage for all document IDs and pre-loads them + async fn restore_documents_from_storage( + &self, + storage: IndexedDbStorage, + ) -> Result<(), String> { + use crate::storage::LocalStorage; + use samod_core::{DocumentId, StorageKey}; + use std::collections::HashSet; + use std::str::FromStr; + + tracing::info!("WASM: Starting document restoration from storage"); + + // Create a prefix to scan for all documents + // Documents are stored with keys like "{doc_id}/snapshot/{hash}" + // We want to find all unique document IDs + let empty_prefix = StorageKey::from_parts(Vec::::new()) + .map_err(|e| format!("Failed to create empty prefix: {}", e))?; + + // Load all keys that match our prefix (which is everything) + let all_keys = storage.load_range(empty_prefix).await; + + // Extract unique document IDs from the keys + let mut document_ids = HashSet::new(); + for (key, _) in all_keys { + // Get the first component of the key, which should be the document ID + let parts: Vec = key.into_iter().map(|key| key.to_string()).collect(); + if !parts.is_empty() { + if let Ok(doc_id) = DocumentId::from_str(&parts[0]) { + document_ids.insert(doc_id); + } + } + } + + tracing::info!("WASM: Found {} documents in storage", document_ids.len()); + + // Pre-load each document by calling find() + // This will create the document actors and load them into memory + for doc_id in document_ids { + tracing::debug!("WASM: Restoring document {}", doc_id); + match self.inner.find(doc_id.clone()).await { + Ok(Some(_)) => { + tracing::debug!("WASM: Successfully restored document {}", doc_id); + } + Ok(None) => { + tracing::warn!( + "WASM: Document {} not found despite being in storage", + doc_id + ); + } + Err(e) => { + tracing::error!("WASM: Failed to restore document {}: {:?}", doc_id, e); + } + } + } + + tracing::info!("WASM: Document restoration complete"); + Ok(()) + } +} + +/// Handle for managing WebSocket connections in WASM +#[wasm_bindgen] +pub struct WasmWebSocketHandle { + close_sender: Option>, + finish_receiver: Option>, +} + +#[wasm_bindgen] +impl WasmWebSocketHandle { + /// Close the WebSocket connection + #[wasm_bindgen] + pub fn close(&mut self) { + if let Some(sender) = self.close_sender.take() { + let _ = sender.send(()); + } + } + + /// Check if the connection has ended and get the reason + /// Returns a Promise that resolves with the disconnect reason or null if still connected + #[wasm_bindgen(js_name = "waitForDisconnect")] + pub fn wait_for_disconnect(&mut self) -> Promise { + if let Some(receiver) = self.finish_receiver.take() { + future_to_promise(async move { + match receiver.await { + Ok(reason) => Ok(JsValue::from_str(&reason)), + Err(_) => Ok(JsValue::NULL), + } + }) + } else { + future_to_promise(async { Ok(JsValue::NULL) }) + } + } +} + +/// WASM wrapper around a Samod document handle +#[wasm_bindgen] +pub struct WasmDocHandle { + inner: DocHandle, +} + +impl WasmDocHandle { + fn new(inner: DocHandle) -> Self { + Self { inner } + } +} + +#[wasm_bindgen] +impl WasmDocHandle { + /// Get the document ID as a string + #[wasm_bindgen(js_name = "documentId")] + pub fn document_id(&self) -> String { + self.inner.document_id().to_string() + } + + /// Get the document URL (compatible with automerge-repo) + #[wasm_bindgen] + pub fn url(&self) -> String { + self.inner.url().to_string() + } + + /// Get the current state of the document as a JavaScript object + #[wasm_bindgen(js_name = "getDocument")] + pub fn get_document(&self) -> Result { + let mut result = None; + self.inner + .with_document(|doc| { + // Convert the automerge document to a JS-friendly format + let js_doc = automerge_to_js_value(doc)?; + result = Some(js_doc); + Ok::<_, automerge::AutomergeError>(()) + }) + .map_err(|e| WasmError::from(format!("Failed to access document: {}", e)))?; + + Ok(result.unwrap_or(JsValue::NULL)) + } +} + +/// Helper function to convert a JavaScript value to Automerge operations +fn js_value_to_automerge + Clone>( + tx: &mut T, + obj: automerge::ObjId, + key: K, + js_value: &JsValue, +) -> Result<(), automerge::AutomergeError> { + use automerge::ScalarValue; + use js_sys::{Array, Object, Uint8Array}; + use wasm_bindgen::JsCast; + + if js_value.is_null() { + tx.put(obj, key, ScalarValue::Null)?; + } else if js_value.is_undefined() { + // Skip undefined values + return Ok(()); + } else if let Some(bool_val) = js_value.as_bool() { + tx.put(obj, key, bool_val)?; + } else if let Some(num_val) = js_value.as_f64() { + if num_val.fract() == 0.0 && num_val >= i64::MIN as f64 && num_val <= i64::MAX as f64 { + tx.put(obj, key, num_val as i64)?; + } else { + tx.put(obj, key, num_val)?; + } + } else if let Some(str_val) = js_value.as_string() { + tx.put(obj, key, str_val)?; + } else if js_value.is_instance_of::() { + // Handle Uint8Array (binary data) + let uint8_array = js_value.dyn_ref::().unwrap(); + let bytes = uint8_array.to_vec(); + tx.put(obj, key, ScalarValue::Bytes(bytes))?; + } else if Array::is_array(js_value) { + let array = Array::from(js_value); + let list_obj = tx.put_object(obj, key.clone(), automerge::ObjType::List)?; + + for i in 0..array.length() { + let item = array.get(i); + js_value_to_automerge(tx, list_obj.clone(), i as usize, &item)?; + } + } else if js_value.is_object() { + let obj_js = Object::from(js_value.clone()); + let map_obj = tx.put_object(obj, key.clone(), automerge::ObjType::Map)?; + + let keys = Object::keys(&obj_js); + for i in 0..keys.length() { + let key_js = keys.get(i); + if let Some(key_str) = key_js.as_string() { + let value = js_sys::Reflect::get(&obj_js, &key_js) + .map_err(|_| automerge::AutomergeError::InvalidOp(automerge::ObjType::Map))?; + js_value_to_automerge(tx, map_obj.clone(), &key_str, &value)?; + } else { + return Err(automerge::AutomergeError::InvalidOp( + automerge::ObjType::Map, + )); + } + } + } else { + // Handle unsupported JavaScript types + return Err(automerge::AutomergeError::InvalidOp( + automerge::ObjType::Map, + )); + } + + Ok(()) +} + +/// Helper function to recursively populate an entire document from a JavaScript object +fn populate_doc_from_js_value( + doc: &mut Automerge, + js_value: &JsValue, +) -> Result<(), automerge::AutomergeError> { + use js_sys::Object; + + if js_value.is_object() && !js_value.is_null() { + let obj_js = Object::from(js_value.clone()); + let keys = Object::keys(&obj_js); + + doc.transact(|tx| { + for i in 0..keys.length() { + let key_js = keys.get(i); + if let Some(key_str) = key_js.as_string() { + let value = js_sys::Reflect::get(&obj_js, &key_js).map_err(|_| { + automerge::AutomergeError::InvalidOp(automerge::ObjType::Map) + })?; + js_value_to_automerge(tx, automerge::ROOT, &key_str, &value)?; + } + } + Ok(()) + }) + .map_err( + |_: automerge::transaction::Failure| { + automerge::AutomergeError::InvalidOp(automerge::ObjType::Map) + }, + )?; + } + + Ok(()) +} + +/// Helper function to convert an Automerge document to a JavaScript value +fn automerge_to_js_value(doc: &Automerge) -> Result { + automerge_obj_to_js_value(doc, automerge::ROOT) +} + +/// Helper function to recursively convert an Automerge object to a JavaScript value +fn automerge_obj_to_js_value( + doc: &Automerge, + obj: automerge::ObjId, +) -> Result { + use automerge::{ObjType, ReadDoc}; + use js_sys::{Array, Object}; + + match doc.object_type(&obj)? { + ObjType::Map => { + let js_obj = Object::new(); + + // Get all properties from the map object + for item in doc.map_range(&obj, ..) { + match item.value { + automerge::ValueRef::Object(_) => { + // For object values, we need to handle them using the object ID + // which is available in the item context + let obj_id = item.id(); + let prop = item.key; + let result = automerge_obj_to_js_value(doc, obj_id)?; + js_sys::Reflect::set(&js_obj, &JsValue::from_str(&prop), &result) + .map_err(|_| automerge::AutomergeError::InvalidOp(ObjType::Map))?; + } + value => { + let prop = item.key; + let result = automerge_value_to_js_value(doc, value.into())?; + js_sys::Reflect::set(&js_obj, &JsValue::from_str(&prop), &result) + .map_err(|_| automerge::AutomergeError::InvalidOp(ObjType::Map))?; + } + }; + } + + Ok(js_obj.into()) + } + ObjType::List => { + let js_array = Array::new(); + let len = doc.length(&obj); + + // Get all items from the list object + for i in 0..len { + if let Ok(Some((value, _))) = doc.get(&obj, i) { + let js_value = automerge_value_to_js_value(doc, value)?; + js_array.push(&js_value); + } + } + + Ok(js_array.into()) + } + ObjType::Text => { + // Convert text object to string + let text = doc.text(&obj)?; + Ok(JsValue::from_str(&text)) + } + ObjType::Table => { + // Tables are handled similar to maps + let js_obj = Object::new(); + + // Get all properties from the table object + for item in doc.map_range(&obj, ..) { + match item.value { + automerge::ValueRef::Object(_) => { + let obj_id = item.id(); + let prop = item.key; + let result = automerge_obj_to_js_value(doc, obj_id)?; + js_sys::Reflect::set(&js_obj, &JsValue::from_str(&prop), &result) + .map_err(|_| automerge::AutomergeError::InvalidOp(ObjType::Table))?; + } + value => { + let prop = item.key; + let result = automerge_value_to_js_value(doc, value.into())?; + js_sys::Reflect::set(&js_obj, &JsValue::from_str(&prop), &result) + .map_err(|_| automerge::AutomergeError::InvalidOp(ObjType::Table))?; + } + }; + } + + Ok(js_obj.into()) + } + } +} + +/// Helper function to convert an Automerge value to a JavaScript value +fn automerge_value_to_js_value( + _doc: &Automerge, + value: automerge::Value<'_>, +) -> Result { + use automerge::Value; + + match value { + Value::Scalar(scalar_value) => match scalar_value.as_ref() { + automerge::ScalarValue::Str(s) => Ok(JsValue::from_str(s.as_str())), + automerge::ScalarValue::Int(i) => Ok(JsValue::from(*i as f64)), + automerge::ScalarValue::Uint(u) => Ok(JsValue::from(*u as f64)), + automerge::ScalarValue::F64(f) => Ok(JsValue::from(*f)), + automerge::ScalarValue::Boolean(b) => Ok(JsValue::from(*b)), + automerge::ScalarValue::Bytes(bytes) => Ok(js_sys::Uint8Array::from(&bytes[..]).into()), + automerge::ScalarValue::Null => Ok(JsValue::NULL), + automerge::ScalarValue::Timestamp(t) => Ok(JsValue::from(*t as f64)), + automerge::ScalarValue::Counter(c) => Ok(JsValue::from(format!("{}", c))), + _ => Ok(JsValue::UNDEFINED), + }, + Value::Object(obj_type) => Err(automerge::AutomergeError::InvalidOp(obj_type.to_owned())), + } +} diff --git a/samod/src/websocket.rs b/samod/src/websocket.rs index c6529d4..5632666 100644 --- a/samod/src/websocket.rs +++ b/samod/src/websocket.rs @@ -1,7 +1,32 @@ use futures::{Future, Sink, SinkExt, Stream, StreamExt}; +#[cfg(feature = "wasm")] +use futures::{ + FutureExt, + channel::{mpsc, oneshot}, +}; +#[cfg(feature = "wasm")] +use js_sys::Uint8Array; +#[cfg(feature = "wasm")] +use std::task::Poll; +#[cfg(feature = "wasm")] +use std::{cell::RefCell, pin::Pin, rc::Rc}; +#[cfg(feature = "wasm")] +use wasm_bindgen::{JsCast, prelude::Closure}; +#[cfg(feature = "wasm")] +use web_sys::{CloseEvent, ErrorEvent, MessageEvent, WebSocket}; use crate::{ConnDirection, ConnFinishedReason, Repo}; +#[cfg(feature = "wasm")] +use futures::future::LocalBoxFuture; + +#[cfg(feature = "wasm")] +pub struct WasmConnectionEvents { + pub on_open: oneshot::Receiver<()>, + pub on_ready: oneshot::Receiver<()>, + pub finished: LocalBoxFuture<'static, ConnFinishedReason>, +} + /// A copy of tungstenite::Message /// /// This is necessary because axum uses tungstenite::Message internally but exposes it's own @@ -107,6 +132,111 @@ impl Repo { self.connect_websocket(stream, ConnDirection::Incoming) } + /// Connect to a WebSocket server from a WASM environment + /// + /// This method creates a WebSocket connection using the browser's native WebSocket API + /// and integrates it with samod's synchronization protocol. + /// + /// # Arguments + /// + /// * `url` - The WebSocket URL to connect to (e.g., "ws://localhost:8080" or "wss://example.com") + /// * `direction` - Whether this is an outgoing or incoming connection + /// + /// # Returns + /// + /// A `ConnFinishedReason` indicating how the connection terminated: + /// - `ConnFinishedReason::Shutdown` - The repo was shut down + /// - `ConnFinishedReason::Error(String)` - An error occurred + /// - Other variants as per normal connection lifecycle + /// + /// # Example + /// + /// ```no_run + /// use samod::{Repo, ConnDirection}; + /// + /// let repo = Repo::build_wasm().load().await; + /// let result = repo.connect_wasm_websocket( + /// "ws://localhost:8080/sync", + /// ConnDirection::Outgoing + /// ).await; + /// ``` + /// + /// # Panics + /// + /// This method must be called from within a WASM environment with access to + /// the browser's WebSocket API. It will panic if called outside of a browser context. + #[cfg(feature = "wasm")] + pub async fn connect_wasm_websocket( + &self, + url: &str, + direction: ConnDirection, + ) -> ConnFinishedReason { + tracing::info!("WASM: Attempting WebSocket connection to {}", url); + tracing::debug!("WASM: Connection direction: {:?}", direction); + + match WasmWebSocket::connect(url).await { + Ok(ws) => { + tracing::info!( + "WASM: WebSocket connection established, starting protocol handshake" + ); + let (sink, stream) = ws.split(); + let result = self.connect(stream, sink, direction).await; + tracing::info!("WASM: Connection finished with reason: {:?}", result); + result + } + Err(e) => { + let error_msg = format!("Failed to connect WebSocket: {}", e); + tracing::error!("WASM: {}", error_msg); + ConnFinishedReason::Error(error_msg) + } + } + } + + #[cfg(feature = "wasm")] + pub fn connect_wasm_websocket_observable( + &self, + url: &str, + direction: ConnDirection, + ) -> WasmConnectionEvents { + let (open_tx, open_rx) = oneshot::channel(); + let (ready_tx, ready_rx) = oneshot::channel(); + + let repo = self.clone(); + let url = url.to_string(); + + let finished = async move { + tracing::info!("WASM: Attempting WebSocket connection to {}", url); + + match WasmWebSocket::connect(&url).await { + Ok(ws) => { + tracing::info!("WASM: WebSocket connection established"); + let _ = open_tx.send(()); + + let (sink, stream) = ws.split(); + + let result = repo + .connect_with_ready_signal(stream, sink, direction, ready_tx) + .await; + + tracing::info!("WASM: Connection finished with reason: {:?}", result); + result + } + Err(e) => { + let error_msg = format!("Failed to connect WebSocket: {}", e); + tracing::error!("WASM: {}", error_msg); + ConnFinishedReason::Error(error_msg) + } + } + } + .boxed_local(); + + WasmConnectionEvents { + on_open: open_rx, + on_ready: ready_rx, + finished, + } + } + /// Connect any stream of [`WsMessage`]s /// /// [`WsMessage`] is a copy of `tungstenite::Message` and @@ -115,6 +245,62 @@ impl Repo { /// are identical, but not the same type. This function allows us to /// implement the connection logic once and use it for both `tungstenite` /// and `axum`. + #[cfg(target_arch = "wasm32")] + pub fn connect_websocket( + &self, + stream: S, + direction: ConnDirection, + ) -> impl Future + 'static + where + M: Into + From + 'static, + S: Sink + Stream> + 'static, + { + let (sink, stream) = stream.split(); + + let msg_stream = stream + .filter_map::<_, Result, NetworkError>, _>({ + move |msg| async move { + let msg = match msg { + Ok(m) => m, + Err(e) => { + return Some(Err(NetworkError(format!( + "websocket receive error: {e}" + )))); + } + }; + match msg.into() { + WsMessage::Binary(data) => Some(Ok(data)), + WsMessage::Close => { + tracing::debug!("websocket closing"); + None + } + WsMessage::Ping(_) | WsMessage::Pong(_) => None, + WsMessage::Text(_) => Some(Err(NetworkError( + "unexpected string message on websocket".to_string(), + ))), + } + } + }) + .boxed_local(); + + let msg_sink = sink + .sink_map_err(|e| NetworkError(format!("websocket send error: {e}"))) + .with(|msg| { + futures::future::ready(Ok::<_, NetworkError>(WsMessage::Binary(msg).into())) + }); + + self.connect(msg_stream, msg_sink, direction) + } + + /// Connect any stream of [`WsMessage`]s + /// + /// [`WsMessage`] is a copy of `tungstenite::Message` and + /// `axum::extract::ws::Message` which is reimplemented in this crate + /// because both `tungstenite` and `axum` use their own message types which + /// are identical, but not the same type. This function allows us to + /// implement the connection logic once and use it for both `tungstenite` + /// and `axum`. + #[cfg(not(target_arch = "wasm32"))] pub fn connect_websocket( &self, stream: S, @@ -174,3 +360,267 @@ impl std::fmt::Display for NetworkError { } } impl std::error::Error for NetworkError {} + +// Hold closures to prevent them from being dropped +#[cfg(feature = "wasm")] +struct ClosureHandlers { + _on_message: Closure, + _on_close: Closure, + _on_error: Closure, + _on_open: Closure, +} + +/// A WebSocket implementation for WASM environments +/// +/// This struct wraps the browser's native WebSocket API and implements +/// the `Stream` and `Sink` traits to work with samod's connection protocol. +/// +/// The WebSocket is configured to: +/// - Use binary messages (ArrayBuffer) for data transfer +/// - Automatically handle connection lifecycle events +/// - Convert browser events into a Stream/Sink interface +/// +/// # Safety +/// +/// This type implements `Send` even though it contains `Rc` and browser objects +/// because WASM is single-threaded. All operations happen on the same thread. +#[cfg(feature = "wasm")] +pub struct WasmWebSocket { + ws: WebSocket, + _closures: ClosureHandlers, + receiver: mpsc::UnboundedReceiver, +} + +#[cfg(feature = "wasm")] +impl WasmWebSocket { + /// Create a new WebSocket connection + /// + /// This method establishes a WebSocket connection to the specified URL and + /// waits for the connection to be fully established before returning. + /// + /// # Arguments + /// + /// * `url` - The WebSocket URL to connect to + /// + /// # Returns + /// + /// * `Ok(WasmWebSocket)` - A connected WebSocket ready for use + /// * `Err(NetworkError)` - If connection fails or is rejected + /// + /// # Connection Process + /// + /// 1. Creates a browser WebSocket instance + /// 2. Sets up event handlers for open, message, error, and close events + /// 3. Waits for either the 'open' event (success) or 'error' event (failure) + /// 4. Returns the connected WebSocket or an error + /// + /// # Example + /// + /// ```no_run + /// let ws = WasmWebSocket::connect("ws://localhost:8080").await?; + /// ``` + pub async fn connect(url: &str) -> Result { + let ws = WebSocket::new(url).map_err(|_| { + NetworkError(format!("error creating websocket connection").to_string()) + })?; + + ws.set_binary_type(web_sys::BinaryType::Arraybuffer); + + // Create channels for messages + let (sender, receiver) = mpsc::unbounded(); + let sender_rc = Rc::new(RefCell::new(sender)); + + // Create a oneshot channel to signal connection status + let (conn_tx, conn_rx) = oneshot::channel::>(); + let conn_tx = Rc::new(RefCell::new(Some(conn_tx))); + + // Set up open handler to signal successful connection + let conn_tx_open = Rc::clone(&conn_tx); + let on_open = Closure::wrap(Box::new(move |_: web_sys::Event| { + if let Some(tx) = conn_tx_open.borrow_mut().take() { + let _ = tx.send(Ok(())); + } + }) as Box); + ws.set_onopen(Some(on_open.as_ref().unchecked_ref())); + + // Set up error handler to signal connection failure + let conn_tx_error = Rc::clone(&conn_tx); + let on_error = Closure::wrap(Box::new(move |e: web_sys::ErrorEvent| { + let error_msg = if e.message().is_empty() { + "WebSocket connection error".to_string() + } else { + e.message() + }; + + if let Some(tx) = conn_tx_error.borrow_mut().take() { + let _ = tx.send(Err(NetworkError(format!( + "WebSocket connection failed: {}", + error_msg + )))); + } + }) as Box); + ws.set_onerror(Some(on_error.as_ref().unchecked_ref())); + + // Set up message handler + let sender_for_msg = Rc::clone(&sender_rc); + let on_message = Closure::wrap(Box::new(move |e: MessageEvent| { + if let Ok(array_buffer) = e.data().dyn_into::() { + let array = Uint8Array::new(&array_buffer); + let bytes = array.to_vec(); + + if let Ok(sender) = sender_for_msg.try_borrow() { + let _ = sender.unbounded_send(WsMessage::Binary(bytes)); + } + } + }) as Box); + ws.set_onmessage(Some(on_message.as_ref().unchecked_ref())); + + // Set up close handler + let sender_for_close = Rc::clone(&sender_rc); + let on_close = Closure::wrap(Box::new(move |_| { + if let Ok(sender) = sender_for_close.try_borrow() { + let _ = sender.unbounded_send(WsMessage::Close); + } + }) as Box); + ws.set_onclose(Some(on_close.as_ref().unchecked_ref())); + + // Wait for connection to complete + let connection_result = conn_rx + .await + .map_err(|_| NetworkError(format!("connection attempt was cancelled").to_string()))?; + + connection_result?; + + let websocket = Self { + ws, + _closures: ClosureHandlers { + _on_message: on_message, + _on_close: on_close, + _on_error: on_error, + _on_open: on_open, + }, + receiver, + }; + + Ok(websocket) + } + + /// Send binary data through the WebSocket + /// + /// # Arguments + /// + /// * `data` - The binary data to send + /// + /// # Returns + /// + /// * `Ok(())` - Data was successfully queued for sending + /// * `Err(NetworkError)` - If the WebSocket is not in a state to send data + /// + /// # Note + /// + /// This method queues data for sending but doesn't guarantee delivery. + /// The actual transmission happens asynchronously. + pub fn send(&self, data: Vec) -> Result<(), NetworkError> { + let array = Uint8Array::from(&data[..]); + self.ws + .send_with_array_buffer(&array.buffer()) + .map_err(|e| NetworkError(format!("failed to send message: {:?}", e)))?; + Ok(()) + } + + /// Close the WebSocket connection + /// + /// This initiates a graceful close of the WebSocket connection. + /// Any pending messages may still be delivered before the connection fully closes. + /// + /// # Returns + /// + /// * `Ok(())` - Close was initiated successfully + /// * `Err(NetworkError)` - If the WebSocket is already closed or in an invalid state + pub fn close(&self) -> Result<(), NetworkError> { + self.ws + .close() + .map_err(|_| NetworkError("Failed to close WebSocket".to_string()))?; + Ok(()) + } +} + +// Safe because WASM in single-threaded +#[cfg(feature = "wasm")] +unsafe impl Send for WasmWebSocket {} + +/// Receives messages from the WebSocket as a Stream +/// +/// # Message Handling +/// +/// - Binary messages: Passed through as-is +/// - Text messages: Converted to errors (protocol violation) +/// - Close messages: Ends the stream +/// - Ping/Pong: Handled internally, not exposed to consumers +#[cfg(feature = "wasm")] +impl Stream for WasmWebSocket { + type Item = Result, NetworkError>; + + fn poll_next( + mut self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match Pin::new(&mut self.receiver).poll_next(cx) { + Poll::Ready(Some(WsMessage::Binary(data))) => Poll::Ready(Some(Ok(data))), + Poll::Ready(Some(WsMessage::Text(_))) => Poll::Ready(Some(Err(NetworkError( + "unexpected text message on websocket".to_string(), + )))), + Poll::Ready(Some(WsMessage::Close)) => Poll::Ready(None), + Poll::Ready(Some(WsMessage::Ping(_)) | Some(WsMessage::Pong(_))) => { + // Skip ping/pong messages and poll again + cx.waker().wake_by_ref(); + Poll::Pending + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + } + } +} + +/// Sends messages to the WebSocket as a Sink +/// +/// This implementation: +/// - Accepts `Vec` binary data +/// - Is always ready to accept messages (buffering handled by browser) +/// - Sends data immediately without internal buffering +/// - Gracefully closes the connection when the sink is closed +/// +/// # Backpressure +/// +/// The browser's WebSocket implementation handles buffering and backpressure. +/// This sink reports as always ready, relying on the browser's internal queue. +#[cfg(feature = "wasm")] +impl Sink> for WasmWebSocket { + type Error = NetworkError; + + fn poll_ready( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Poll::Ready(Ok(())) + } + + fn start_send(self: Pin<&mut Self>, item: Vec) -> Result<(), Self::Error> { + self.as_ref().send(item) + } + + fn poll_flush( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + Poll::Ready(Ok(())) + } + + fn poll_close( + self: Pin<&mut Self>, + _cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + let _ = self.as_ref().close(); + Poll::Ready(Ok(())) + } +} diff --git a/scripts/build-wasm.sh b/scripts/build-wasm.sh new file mode 100755 index 0000000..1cda64e --- /dev/null +++ b/scripts/build-wasm.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -e + +# Ensure wasm-pack is installed +if ! command -v wasm-pack &>/dev/null; then + echo "Error: wasm-pack is not installed" + echo "Please install wasm-pack: cargo install wasm-pack" + exit 1 +fi + +# Build directory setup +WASM_OUT_DIR="wasm-tests/pkg" +mkdir -p "$WASM_OUT_DIR" + +echo "Building samod WASM package..." + +# Build the WASM package with the wasm feature enabled +cd samod +wasm-pack build \ + --target web \ + --out-dir "../$WASM_OUT_DIR" \ + --features wasm \ + --no-default-features + +cd .. + +echo "WASM build complete! Output in $WASM_OUT_DIR" + +# Generate TypeScript types if requested +if [[ "$3" == "--typescript" ]]; then + echo "Generating TypeScript definitions..." + # The TypeScript definitions are automatically generated by wasm-pack + echo "TypeScript definitions generated in $WASM_OUT_DIR" +fi + diff --git a/scripts/test-wasm.sh b/scripts/test-wasm.sh new file mode 100755 index 0000000..f29cf93 --- /dev/null +++ b/scripts/test-wasm.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# Script to run WASM tests for samod +# Builds the WASM package and runs Playwright tests + +set -e + +echo "Running samod WASM tests..." + +# Navigate to wasm-tests directory +cd "$(dirname "$0")/../wasm-tests" + +# Install dependencies if needed +if [ ! -d "node_modules" ]; then + echo "Installing dependencies..." + npm install +fi + +# Build WASM package +echo "Building WASM package..." +npm run build-wasm + +# Start the test server in WASM mode +echo "Starting test server in WASM mode..." +npm run server:stop +npm run server:start + +# Wait for server to start +sleep 2 + +# Run Playwright tests +echo "Running Playwright tests..." +npm test + +# Stop the server +echo "Stopping test server..." +npm run server:stop + +echo "✅ WASM tests completed!" + diff --git a/wasm-tests/.gitignore b/wasm-tests/.gitignore new file mode 100644 index 0000000..c8d90d6 --- /dev/null +++ b/wasm-tests/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +pkg/ +test-results/ +playwright-report/ +playwright/.cache/ +.vite/ \ No newline at end of file diff --git a/wasm-tests/.prettierrc.json b/wasm-tests/.prettierrc.json new file mode 100644 index 0000000..7e91fdc --- /dev/null +++ b/wasm-tests/.prettierrc.json @@ -0,0 +1,27 @@ +{ + "semi": true, + "trailingComma": "es5", + "singleQuote": true, + "printWidth": 80, + "tabWidth": 2, + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid", + "endOfLine": "lf", + "overrides": [ + { + "files": "*.md", + "options": { + "printWidth": 100, + "proseWrap": "always" + } + }, + { + "files": "*.json", + "options": { + "printWidth": 120 + } + } + ] +} + diff --git a/wasm-tests/package-lock.json b/wasm-tests/package-lock.json new file mode 100644 index 0000000..817dfaf --- /dev/null +++ b/wasm-tests/package-lock.json @@ -0,0 +1,1337 @@ +{ + "name": "samod-wasm-tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "samod-wasm-tests", + "version": "1.0.0", + "dependencies": { + "@automerge/automerge": "^2.2.9", + "@automerge/automerge-repo": "^2.0.7", + "@automerge/automerge-repo-network-websocket": "^2.0.7", + "@automerge/automerge-repo-storage-indexeddb": "^2.0.7" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vite": "^5.4.0" + } + }, + "node_modules/@automerge/automerge": { + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@automerge/automerge/-/automerge-2.2.9.tgz", + "integrity": "sha512-6HM52Ops79hAQBWMg/t0MNfGOdEiXyenQjO9F1hKZq0RWDsMLpPa1SzRy/C4/4UyX67sTHuA5CwBpH34SpfZlA==", + "license": "MIT", + "dependencies": { + "uuid": "^9.0.0" + } + }, + "node_modules/@automerge/automerge-repo": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@automerge/automerge-repo/-/automerge-repo-2.3.0.tgz", + "integrity": "sha512-nZppJl90bK0zVGtByHDvXZ1gC8HCcKnhGkJxMLrPxuFlLdoAfq7x10Tk6AT0eMfx1048kl5zRDH7DpAZD+SvMw==", + "license": "MIT", + "dependencies": { + "@automerge/automerge": "2.2.8 - 3", + "bs58check": "^3.0.1", + "cbor-x": "^1.3.0", + "debug": "^4.3.4", + "eventemitter3": "^5.0.1", + "fast-sha256": "^1.3.0", + "uuid": "^9.0.0", + "xstate": "^5.9.1" + } + }, + "node_modules/@automerge/automerge-repo-network-websocket": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@automerge/automerge-repo-network-websocket/-/automerge-repo-network-websocket-2.3.0.tgz", + "integrity": "sha512-1NddZXFu9Ry2Pfy7p/wbvG8TcsPvv87NPhD7+aiLb8miWrk1TjHYEUbwRQPMUN2lKAcDtnbpMZbJxbwgItxFWA==", + "license": "MIT", + "dependencies": { + "@automerge/automerge-repo": "2.3.0", + "cbor-x": "^1.3.0", + "debug": "^4.3.4", + "eventemitter3": "^5.0.1", + "isomorphic-ws": "^5.0.0", + "ws": "^8.7.0" + } + }, + "node_modules/@automerge/automerge-repo-storage-indexeddb": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@automerge/automerge-repo-storage-indexeddb/-/automerge-repo-storage-indexeddb-2.3.0.tgz", + "integrity": "sha512-0amBVYn9Y/73fUVPvSUQWlg6kv6BKCEPL2C4omA1Z9L7N1DHUXCKje6WuuoF9eIKoWHA/n1QFMRj88MGILXcRQ==", + "license": "MIT", + "dependencies": { + "@automerge/automerge-repo": "2.3.0" + } + }, + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@playwright/test": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.55.0.tgz", + "integrity": "sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz", + "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz", + "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz", + "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz", + "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz", + "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz", + "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz", + "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz", + "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz", + "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz", + "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz", + "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz", + "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz", + "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz", + "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz", + "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz", + "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz", + "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz", + "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz", + "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz", + "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz", + "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.18.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.1.tgz", + "integrity": "sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, + "node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, + "node_modules/cbor-extract": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.1.1" + }, + "bin": { + "download-cbor-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", + "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", + "@cbor-extract/cbor-extract-linux-x64": "2.2.0", + "@cbor-extract/cbor-extract-win32-x64": "2.2.0" + } + }, + "node_modules/cbor-x": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz", + "integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==", + "license": "MIT", + "optionalDependencies": { + "cbor-extract": "^2.2.0" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/playwright": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz", + "integrity": "sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.55.0" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz", + "integrity": "sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.50.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz", + "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.50.1", + "@rollup/rollup-android-arm64": "4.50.1", + "@rollup/rollup-darwin-arm64": "4.50.1", + "@rollup/rollup-darwin-x64": "4.50.1", + "@rollup/rollup-freebsd-arm64": "4.50.1", + "@rollup/rollup-freebsd-x64": "4.50.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.50.1", + "@rollup/rollup-linux-arm-musleabihf": "4.50.1", + "@rollup/rollup-linux-arm64-gnu": "4.50.1", + "@rollup/rollup-linux-arm64-musl": "4.50.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.50.1", + "@rollup/rollup-linux-ppc64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-gnu": "4.50.1", + "@rollup/rollup-linux-riscv64-musl": "4.50.1", + "@rollup/rollup-linux-s390x-gnu": "4.50.1", + "@rollup/rollup-linux-x64-gnu": "4.50.1", + "@rollup/rollup-linux-x64-musl": "4.50.1", + "@rollup/rollup-openharmony-arm64": "4.50.1", + "@rollup/rollup-win32-arm64-msvc": "4.50.1", + "@rollup/rollup-win32-ia32-msvc": "4.50.1", + "@rollup/rollup-win32-x64-msvc": "4.50.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/vite": { + "version": "5.4.20", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz", + "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xstate": { + "version": "5.21.0", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.21.0.tgz", + "integrity": "sha512-y4wmqxjyAa0tgz4k3m/MgTF1kDOahE5+xLfWt5eh1sk+43DatLhKlI8lQDJZpvihZavjbD3TUgy2PRMphhhqgQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + } + } +} diff --git a/wasm-tests/package.json b/wasm-tests/package.json new file mode 100644 index 0000000..33720e2 --- /dev/null +++ b/wasm-tests/package.json @@ -0,0 +1,34 @@ +{ + "name": "samod-wasm-tests", + "version": "1.0.0", + "description": "Browser tests for samod WASM build", + "private": true, + "type": "module", + "scripts": { + "build": "tsc", + "build-wasm": "cd .. && ./scripts/build-wasm.sh --features wasm --typescript", + "test": "playwright test", + "test:ui": "playwright test --ui", + "test:debug": "playwright test --debug", + "test:headed": "playwright test --headed", + "test:all": "npm run build-wasm && npm run test", + "serve": "vite", + "preview": "vite preview", + "server": "node ../samod/interop-test-server/server.js --wasm", + "server:start": "node ../samod/interop-test-server/server.js --wasm &", + "server:stop": "pkill -f 'interop-test-server/server.js --wasm' || true", + "test:with-server": "npm run server:stop && npm run server:start && sleep 2 && npm run test; npm run server:stop" + }, + "devDependencies": { + "@playwright/test": "^1.48.0", + "@types/node": "^22.0.0", + "typescript": "^5.7.0", + "vite": "^5.4.0" + }, + "dependencies": { + "@automerge/automerge": "^2.2.9", + "@automerge/automerge-repo": "^2.0.7", + "@automerge/automerge-repo-network-websocket": "^2.0.7", + "@automerge/automerge-repo-storage-indexeddb": "^2.0.7" + } +} diff --git a/wasm-tests/playwright.config.ts b/wasm-tests/playwright.config.ts new file mode 100644 index 0000000..d1bd351 --- /dev/null +++ b/wasm-tests/playwright.config.ts @@ -0,0 +1,37 @@ +import { defineConfig, devices } from '@playwright/test'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + + use: { + baseURL: 'http://localhost:3000', + trace: 'on-first-retry', + screenshot: 'only-on-failure', + }, + + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + { + name: 'firefox', + use: { ...devices['Desktop Firefox'] }, + }, + { + name: 'webkit', + use: { ...devices['Desktop Safari'] }, + }, + ], + + webServer: { + command: 'npm run serve', + port: 3000, + reuseExistingServer: !process.env.CI, + }, +}); diff --git a/wasm-tests/pnpm-lock.yaml b/wasm-tests/pnpm-lock.yaml new file mode 100644 index 0000000..3770a27 --- /dev/null +++ b/wasm-tests/pnpm-lock.yaml @@ -0,0 +1,835 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@automerge/automerge': + specifier: ^2.2.9 + version: 2.2.9 + '@automerge/automerge-repo': + specifier: ^2.0.7 + version: 2.3.0 + '@automerge/automerge-repo-network-websocket': + specifier: ^2.0.7 + version: 2.3.0 + '@automerge/automerge-repo-storage-indexeddb': + specifier: ^2.0.7 + version: 2.3.0 + devDependencies: + '@playwright/test': + specifier: ^1.48.0 + version: 1.55.0 + '@types/node': + specifier: ^22.0.0 + version: 22.18.1 + typescript: + specifier: ^5.7.0 + version: 5.9.2 + vite: + specifier: ^5.4.0 + version: 5.4.20(@types/node@22.18.1) + +packages: + + '@automerge/automerge-repo-network-websocket@2.3.0': + resolution: {integrity: sha512-1NddZXFu9Ry2Pfy7p/wbvG8TcsPvv87NPhD7+aiLb8miWrk1TjHYEUbwRQPMUN2lKAcDtnbpMZbJxbwgItxFWA==} + + '@automerge/automerge-repo-storage-indexeddb@2.3.0': + resolution: {integrity: sha512-0amBVYn9Y/73fUVPvSUQWlg6kv6BKCEPL2C4omA1Z9L7N1DHUXCKje6WuuoF9eIKoWHA/n1QFMRj88MGILXcRQ==} + + '@automerge/automerge-repo@2.3.0': + resolution: {integrity: sha512-nZppJl90bK0zVGtByHDvXZ1gC8HCcKnhGkJxMLrPxuFlLdoAfq7x10Tk6AT0eMfx1048kl5zRDH7DpAZD+SvMw==} + + '@automerge/automerge@2.2.9': + resolution: {integrity: sha512-6HM52Ops79hAQBWMg/t0MNfGOdEiXyenQjO9F1hKZq0RWDsMLpPa1SzRy/C4/4UyX67sTHuA5CwBpH34SpfZlA==} + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.0': + resolution: {integrity: sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==} + cpu: [arm64] + os: [darwin] + + '@cbor-extract/cbor-extract-darwin-x64@2.2.0': + resolution: {integrity: sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==} + cpu: [x64] + os: [darwin] + + '@cbor-extract/cbor-extract-linux-arm64@2.2.0': + resolution: {integrity: sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==} + cpu: [arm64] + os: [linux] + + '@cbor-extract/cbor-extract-linux-arm@2.2.0': + resolution: {integrity: sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==} + cpu: [arm] + os: [linux] + + '@cbor-extract/cbor-extract-linux-x64@2.2.0': + resolution: {integrity: sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==} + cpu: [x64] + os: [linux] + + '@cbor-extract/cbor-extract-win32-x64@2.2.0': + resolution: {integrity: sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==} + cpu: [x64] + os: [win32] + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@playwright/test@1.55.0': + resolution: {integrity: sha512-04IXzPwHrW69XusN/SIdDdKZBzMfOT9UNT/YiJit/xpy2VuAoB8NHc8Aplb96zsWDddLnbkPL3TsmrS04ZU2xQ==} + engines: {node: '>=18'} + hasBin: true + + '@rollup/rollup-android-arm-eabi@4.50.1': + resolution: {integrity: sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.50.1': + resolution: {integrity: sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.50.1': + resolution: {integrity: sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.50.1': + resolution: {integrity: sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.50.1': + resolution: {integrity: sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.50.1': + resolution: {integrity: sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + resolution: {integrity: sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + resolution: {integrity: sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.50.1': + resolution: {integrity: sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.50.1': + resolution: {integrity: sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + resolution: {integrity: sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + resolution: {integrity: sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + resolution: {integrity: sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.50.1': + resolution: {integrity: sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.50.1': + resolution: {integrity: sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.50.1': + resolution: {integrity: sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.50.1': + resolution: {integrity: sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.50.1': + resolution: {integrity: sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + resolution: {integrity: sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.50.1': + resolution: {integrity: sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.50.1': + resolution: {integrity: sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==} + cpu: [x64] + os: [win32] + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@22.18.1': + resolution: {integrity: sha512-rzSDyhn4cYznVG+PCzGe1lwuMYJrcBS1fc3JqSa2PvtABwWo+dZ1ij5OVok3tqfpEBCBoaR4d7upFJk73HRJDw==} + + base-x@4.0.1: + resolution: {integrity: sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==} + + bs58@5.0.0: + resolution: {integrity: sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==} + + bs58check@3.0.1: + resolution: {integrity: sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==} + + cbor-extract@2.2.0: + resolution: {integrity: sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==} + hasBin: true + + cbor-x@1.6.0: + resolution: {integrity: sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-gyp-build-optional-packages@5.1.1: + resolution: {integrity: sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==} + hasBin: true + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + playwright-core@1.55.0: + resolution: {integrity: sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.55.0: + resolution: {integrity: sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==} + engines: {node: '>=18'} + hasBin: true + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + rollup@4.50.1: + resolution: {integrity: sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + vite@5.4.20: + resolution: {integrity: sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xstate@5.21.0: + resolution: {integrity: sha512-y4wmqxjyAa0tgz4k3m/MgTF1kDOahE5+xLfWt5eh1sk+43DatLhKlI8lQDJZpvihZavjbD3TUgy2PRMphhhqgQ==} + +snapshots: + + '@automerge/automerge-repo-network-websocket@2.3.0': + dependencies: + '@automerge/automerge-repo': 2.3.0 + cbor-x: 1.6.0 + debug: 4.4.1 + eventemitter3: 5.0.1 + isomorphic-ws: 5.0.0(ws@8.18.3) + ws: 8.18.3 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@automerge/automerge-repo-storage-indexeddb@2.3.0': + dependencies: + '@automerge/automerge-repo': 2.3.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge-repo@2.3.0': + dependencies: + '@automerge/automerge': 2.2.9 + bs58check: 3.0.1 + cbor-x: 1.6.0 + debug: 4.4.1 + eventemitter3: 5.0.1 + fast-sha256: 1.3.0 + uuid: 9.0.1 + xstate: 5.21.0 + transitivePeerDependencies: + - supports-color + + '@automerge/automerge@2.2.9': + dependencies: + uuid: 9.0.1 + + '@cbor-extract/cbor-extract-darwin-arm64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-darwin-x64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-arm64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-arm@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-linux-x64@2.2.0': + optional: true + + '@cbor-extract/cbor-extract-win32-x64@2.2.0': + optional: true + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@noble/hashes@1.8.0': {} + + '@playwright/test@1.55.0': + dependencies: + playwright: 1.55.0 + + '@rollup/rollup-android-arm-eabi@4.50.1': + optional: true + + '@rollup/rollup-android-arm64@4.50.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.50.1': + optional: true + + '@rollup/rollup-darwin-x64@4.50.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.50.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.50.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.50.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.50.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.50.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.50.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.50.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.50.1': + optional: true + + '@rollup/rollup-openharmony-arm64@4.50.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.50.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.50.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.50.1': + optional: true + + '@types/estree@1.0.8': {} + + '@types/node@22.18.1': + dependencies: + undici-types: 6.21.0 + + base-x@4.0.1: {} + + bs58@5.0.0: + dependencies: + base-x: 4.0.1 + + bs58check@3.0.1: + dependencies: + '@noble/hashes': 1.8.0 + bs58: 5.0.0 + + cbor-extract@2.2.0: + dependencies: + node-gyp-build-optional-packages: 5.1.1 + optionalDependencies: + '@cbor-extract/cbor-extract-darwin-arm64': 2.2.0 + '@cbor-extract/cbor-extract-darwin-x64': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm': 2.2.0 + '@cbor-extract/cbor-extract-linux-arm64': 2.2.0 + '@cbor-extract/cbor-extract-linux-x64': 2.2.0 + '@cbor-extract/cbor-extract-win32-x64': 2.2.0 + optional: true + + cbor-x@1.6.0: + optionalDependencies: + cbor-extract: 2.2.0 + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + detect-libc@2.0.4: + optional: true + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + eventemitter3@5.0.1: {} + + fast-sha256@1.3.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + isomorphic-ws@5.0.0(ws@8.18.3): + dependencies: + ws: 8.18.3 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + node-gyp-build-optional-packages@5.1.1: + dependencies: + detect-libc: 2.0.4 + optional: true + + picocolors@1.1.1: {} + + playwright-core@1.55.0: {} + + playwright@1.55.0: + dependencies: + playwright-core: 1.55.0 + optionalDependencies: + fsevents: 2.3.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + rollup@4.50.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.50.1 + '@rollup/rollup-android-arm64': 4.50.1 + '@rollup/rollup-darwin-arm64': 4.50.1 + '@rollup/rollup-darwin-x64': 4.50.1 + '@rollup/rollup-freebsd-arm64': 4.50.1 + '@rollup/rollup-freebsd-x64': 4.50.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.50.1 + '@rollup/rollup-linux-arm-musleabihf': 4.50.1 + '@rollup/rollup-linux-arm64-gnu': 4.50.1 + '@rollup/rollup-linux-arm64-musl': 4.50.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.50.1 + '@rollup/rollup-linux-ppc64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-gnu': 4.50.1 + '@rollup/rollup-linux-riscv64-musl': 4.50.1 + '@rollup/rollup-linux-s390x-gnu': 4.50.1 + '@rollup/rollup-linux-x64-gnu': 4.50.1 + '@rollup/rollup-linux-x64-musl': 4.50.1 + '@rollup/rollup-openharmony-arm64': 4.50.1 + '@rollup/rollup-win32-arm64-msvc': 4.50.1 + '@rollup/rollup-win32-ia32-msvc': 4.50.1 + '@rollup/rollup-win32-x64-msvc': 4.50.1 + fsevents: 2.3.3 + + source-map-js@1.2.1: {} + + typescript@5.9.2: {} + + undici-types@6.21.0: {} + + uuid@9.0.1: {} + + vite@5.4.20(@types/node@22.18.1): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.50.1 + optionalDependencies: + '@types/node': 22.18.1 + fsevents: 2.3.3 + + ws@8.18.3: {} + + xstate@5.21.0: {} diff --git a/wasm-tests/src/app.ts b/wasm-tests/src/app.ts new file mode 100644 index 0000000..ccd5bed --- /dev/null +++ b/wasm-tests/src/app.ts @@ -0,0 +1,480 @@ +import init, { + WasmRepo, + WasmDocHandle, + WasmWebSocketHandle, +} from '../pkg/samod.js'; + +interface AppState { + repo?: WasmRepo; + documents: Map; + isConnected: boolean; + messageCount: number; + connectionHandle?: WasmWebSocketHandle; + reconnectAttempts: number; + reconnectTimeoutId?: number; + autoReconnect: boolean; +} + +class SamodTestApp { + private state: AppState = { + documents: new Map(), + isConnected: false, + messageCount: 0, + reconnectAttempts: 0, + autoReconnect: true, + }; + + private logElement: HTMLElement; + private statusElement: HTMLElement; + private documentListElement: HTMLElement; + + constructor() { + this.logElement = document.getElementById('log')!; + this.statusElement = document.getElementById('status')!; + this.documentListElement = document.getElementById('documentList')!; + + this.initializeApp(); + } + + private async initializeApp() { + try { + this.log('Initializing WASM module...', 'info'); + await init(); + + this.log('WASM module loaded successfully', 'success'); + this.updateStatus('WASM Ready - Not Connected', 'error'); + + // Create samod repository with IndexedDB storage + this.state.repo = await new WasmRepo(); + this.log('Samod repository created with IndexedDB storage', 'success'); + + // Discover any documents that were persisted in IndexedDB + await this.discoverDocuments(); + + // Set up event handlers + this.setupEventHandlers(); + + // Enable buttons + this.enableControls(true); + + // Expose repository and app for testing + (window as any).testRepo = this.state.repo; + (window as any).testApp = this; + } catch (error) { + this.log(`Failed to initialize: ${error}`, 'error'); + this.updateStatus('Initialization Failed', 'error'); + } + } + + private setupEventHandlers() { + // Connection buttons + document + .getElementById('connectBtn')! + .addEventListener('click', () => this.connect()); + document + .getElementById('disconnectBtn')! + .addEventListener('click', () => this.disconnect()); + + // Document operations + document + .getElementById('createDocBtn')! + .addEventListener('click', () => this.createDocument()); + document + .getElementById('syncBtn')! + .addEventListener('click', () => this.syncDocuments()); + } + + private async connect() { + try { + this.log('Connecting to WebSocket server...', 'info'); + this.updateStatus('Connecting...', 'connecting'); + + if (!this.state.repo) { + throw new Error('Repository not initialized'); + } + + // Clear any pending reconnection timeout + if (this.state.reconnectTimeoutId) { + clearTimeout(this.state.reconnectTimeoutId); + this.state.reconnectTimeoutId = undefined; + } + + // Get WebSocket port from URL params or use default + const urlParams = new URLSearchParams(window.location.search); + const wsPort = urlParams.get('wsPort') || '3001'; + const wsUrl = `ws://localhost:${wsPort}`; + this.log( + `Connecting to ${wsUrl} (attempt ${this.state.reconnectAttempts + 1})`, + 'info' + ); + + // Create the WebSocket connection handle + const connectionHandle = this.state.repo.connectWebSocket(wsUrl); + this.state.connectionHandle = connectionHandle; + + // Connection successful - reset reconnect attempts + this.state.reconnectAttempts = 0; + this.state.isConnected = true; + this.updateStatus('Connected', 'connected'); + this.log('WebSocket connection established', 'success'); + + // Monitor for disconnection + this.monitorConnection(connectionHandle); + + // Update button states + document.getElementById('connectBtn')!.setAttribute('disabled', 'true'); + document.getElementById('disconnectBtn')!.removeAttribute('disabled'); + } catch (error) { + this.log(`Connection failed: ${error}`, 'error'); + this.updateStatus('Connection Failed', 'error'); + + // Trigger reconnection with exponential backoff if auto-reconnect is enabled + if (this.state.autoReconnect) { + this.scheduleReconnect(); + } + } + } + + private scheduleReconnect() { + this.state.reconnectAttempts++; + + // Exponential backoff: 1s, 2s, 4s, 8s, 16s, max 30s + const baseDelay = 1000; + const maxDelay = 30000; + const delay = Math.min( + baseDelay * Math.pow(2, this.state.reconnectAttempts - 1), + maxDelay + ); + + this.log( + `Scheduling reconnection in ${delay / 1000}s (attempt ${this.state.reconnectAttempts})`, + 'info' + ); + this.updateStatus( + `Reconnecting in ${Math.ceil(delay / 1000)}s...`, + 'connecting' + ); + + this.state.reconnectTimeoutId = window.setTimeout(() => { + if (this.state.autoReconnect && !this.state.isConnected) { + this.connect(); + } + }, delay); + } + + private enableAutoReconnect(enable: boolean) { + this.state.autoReconnect = enable; + this.log(`Auto-reconnect ${enable ? 'enabled' : 'disabled'}`, 'info'); + + if (!enable && this.state.reconnectTimeoutId) { + clearTimeout(this.state.reconnectTimeoutId); + this.state.reconnectTimeoutId = undefined; + } + } + + private async disconnect() { + try { + this.log('Disconnecting from WebSocket server...', 'info'); + + // Disable auto-reconnect for manual disconnection + this.enableAutoReconnect(false); + + // Close the WebSocket connection using the handle + if (this.state.connectionHandle) { + this.state.connectionHandle.close(); + this.state.connectionHandle = undefined; + } + + this.state.isConnected = false; + this.state.reconnectAttempts = 0; + this.updateStatus('Disconnected', 'error'); + this.log('Disconnected from WebSocket server', 'info'); + + // Update button states + document.getElementById('connectBtn')!.removeAttribute('disabled'); + document + .getElementById('disconnectBtn')! + .setAttribute('disabled', 'true'); + } catch (error) { + this.log(`Disconnect failed: ${error}`, 'error'); + } + } + + private async createDocument() { + try { + const titleInput = document.getElementById( + 'docTitle' + ) as HTMLInputElement; + const contentInput = document.getElementById( + 'docContent' + ) as HTMLTextAreaElement; + + const title = + titleInput.value || `Document ${this.state.documents.size + 1}`; + const content = contentInput.value || 'Empty document'; + + this.log(`Creating document: ${title}`, 'info'); + + if (!this.state.repo) { + throw new Error('Repository not initialized'); + } + + // Create new document with initial content + const initialContent = { + title: title, + content: content, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + const docHandle = await this.state.repo.createDocument(initialContent); + const docId = docHandle.documentId(); + + // Store document reference + this.state.documents.set(docId, docHandle); + + this.log(`Document created: ${docId}`, 'success'); + this.updateDocumentList(); + this.updateMetrics(); + + // Clear inputs + titleInput.value = ''; + contentInput.value = ''; + } catch (error) { + this.log(`Failed to create document: ${error}`, 'error'); + } + } + + private async syncDocuments() { + try { + this.log('Syncing documents...', 'info'); + const startTime = performance.now(); + + if (!this.state.repo) { + throw new Error('Repository not initialized'); + } + + // Discover any new documents from IndexedDB or network sync + await this.discoverDocuments(); + + // Verify all known documents are accessible + let accessibleDocs = 0; + for (const [docId, docHandle] of this.state.documents) { + try { + const doc = docHandle.getDocument(); + if (doc) { + accessibleDocs++; + } + } catch (e) { + this.log(`Document ${docId} not accessible`, 'error'); + } + } + + const syncTime = Math.round(performance.now() - startTime); + this.log( + `Sync completed in ${syncTime}ms - ${accessibleDocs} documents accessible`, + 'success' + ); + + // Always update sync time metrics + const syncTimeElement = document.getElementById('syncTime'); + if (syncTimeElement) { + syncTimeElement.textContent = syncTime.toString(); + } + } catch (error) { + this.log(`Sync failed: ${error}`, 'error'); + } + } + + private updateDocumentList() { + this.documentListElement.innerHTML = ''; + + this.state.documents.forEach((docHandle, id) => { + try { + const doc = docHandle.getDocument(); + const title = doc?.title || 'Untitled Document'; + + const item = document.createElement('li'); + item.className = 'document-item'; + item.innerHTML = ` + ${title} +
+ ID: ${id.substring(0, 8)}... + `; + item.addEventListener('click', () => this.selectDocument(id)); + this.documentListElement.appendChild(item); + } catch (error) { + this.log(`Error displaying document ${id}: ${error}`, 'error'); + } + }); + } + + private selectDocument(docId: string) { + // Update UI to show selected document + const items = this.documentListElement.querySelectorAll('.document-item'); + items.forEach(item => item.classList.remove('active')); + + const selectedIndex = Array.from(this.state.documents.keys()).indexOf( + docId + ); + if (selectedIndex >= 0) { + items[selectedIndex].classList.add('active'); + } + + this.log(`Selected document: ${docId}`, 'info'); + } + + private updateMetrics() { + document.getElementById('docCount')!.textContent = + this.state.documents.size.toString(); + document.getElementById('messageCount')!.textContent = + this.state.messageCount.toString(); + + // Estimate storage size (rough calculation) + const storageSize = this.state.documents.size * 2; // Assume ~2KB per doc + document.getElementById('storageSize')!.textContent = `${storageSize} KB`; + } + + private updateStatus( + message: string, + className: 'connecting' | 'connected' | 'error' + ) { + this.statusElement.textContent = message; + this.statusElement.className = `status ${className}`; + } + + private log(message: string, type: 'info' | 'success' | 'error' = 'info') { + const entry = document.createElement('div'); + entry.className = `log-entry ${type}`; + entry.textContent = `[${new Date().toLocaleTimeString()}] ${message}`; + this.logElement.appendChild(entry); + this.logElement.scrollTop = this.logElement.scrollHeight; + } + + private enableControls(enabled: boolean) { + const buttons = ['connectBtn', 'createDocBtn', 'syncBtn']; + buttons.forEach(id => { + const btn = document.getElementById(id); + if (btn) { + if (enabled) { + btn.removeAttribute('disabled'); + } else { + btn.setAttribute('disabled', 'true'); + } + } + }); + } + + private async discoverDocuments() { + try { + if (!this.state.repo) { + return; + } + + this.log('Discovering available documents...', 'info'); + + const documentIds = this.state.repo.listDocuments(); + this.log(`Found ${documentIds.length} document(s) in repository`, 'info'); + + let newDocuments = 0; + for (let i = 0; i < documentIds.length; i++) { + const docId = documentIds[i]; + if (!this.state.documents.has(docId)) { + try { + const docHandle = await this.state.repo.findDocument(docId); + if (docHandle) { + this.state.documents.set(docId, docHandle); + newDocuments++; + this.log(`Loaded document: ${docId}`, 'success'); + } + } catch (error) { + this.log(`Failed to load document ${docId}: ${error}`, 'error'); + } + } + } + + if (newDocuments > 0) { + this.log(`Discovered ${newDocuments} new document(s)`, 'success'); + this.updateDocumentList(); + this.updateMetrics(); + } else { + this.log('No new documents found', 'info'); + } + } catch (error) { + this.log(`Document discovery failed: ${error}`, 'error'); + } + } + + private async monitorConnection(connectionHandle: WasmWebSocketHandle) { + try { + // Wait for the connection to end + const disconnectReason = await connectionHandle.waitForDisconnect(); + + if (disconnectReason) { + this.log(`Connection ended: ${disconnectReason}`, 'info'); + + // Update UI state if this wasn't a client-initiated disconnect + if (disconnectReason !== 'we_disconnected' && this.state.isConnected) { + this.state.isConnected = false; + this.updateStatus('Disconnected', 'error'); + this.log('Detected server-side disconnection', 'error'); + + // Update button states + document.getElementById('connectBtn')!.removeAttribute('disabled'); + document + .getElementById('disconnectBtn')! + .setAttribute('disabled', 'true'); + + // Clear connection handle + this.state.connectionHandle = undefined; + + // Re-enable auto-reconnect and schedule reconnection for unexpected disconnections + if (disconnectReason !== 'we_disconnected') { + this.enableAutoReconnect(true); + this.scheduleReconnect(); + } + } + } + } catch (error) { + this.log(`Connection monitoring error: ${error}`, 'error'); + // On monitoring error, also try to reconnect + if (this.state.isConnected && this.state.autoReconnect) { + this.state.isConnected = false; + this.updateStatus('Connection Error', 'error'); + this.scheduleReconnect(); + } + } + } + + // Public methods for testing + public getConnectionState() { + return { + isConnected: this.state.isConnected, + reconnectAttempts: this.state.reconnectAttempts, + autoReconnect: this.state.autoReconnect, + hasReconnectTimeout: !!this.state.reconnectTimeoutId, + }; + } + + public setAutoReconnect(enable: boolean) { + this.enableAutoReconnect(enable); + } + + public forceReconnect() { + if (!this.state.isConnected) { + this.state.reconnectAttempts = 0; + this.connect(); + } + } + + public simulateDisconnection() { + if (this.state.connectionHandle) { + this.state.connectionHandle.close(); + } + } +} + +// Initialize app when DOM is ready +document.addEventListener('DOMContentLoaded', () => { + new SamodTestApp(); +}); diff --git a/wasm-tests/src/index.html b/wasm-tests/src/index.html new file mode 100644 index 0000000..d222b8d --- /dev/null +++ b/wasm-tests/src/index.html @@ -0,0 +1,211 @@ + + + + + + Samod WASM Test Application + + + +

Samod WASM Test Application

+ +
+

Connection Status

+
+ Initializing WASM module... +
+ + +
+ +
+

Document Operations

+ + + + + +

Documents

+
    +
    + +
    +

    Performance Metrics

    +
    +
    +
    0
    +
    Documents
    +
    +
    +
    -
    +
    Last Sync (ms)
    +
    +
    +
    0 KB
    +
    Storage Used
    +
    +
    +
    0
    +
    Messages Sent
    +
    +
    +
    + +
    +

    Activity Log

    +
    +
    + + + + diff --git a/wasm-tests/tests/samod-wasm.spec.ts b/wasm-tests/tests/samod-wasm.spec.ts new file mode 100644 index 0000000..ecc2da8 --- /dev/null +++ b/wasm-tests/tests/samod-wasm.spec.ts @@ -0,0 +1,576 @@ +import { test, expect, Page } from '@playwright/test'; +import { TestServer, createTestServer } from './server-helper'; + +// Helper functions for direct repo API access +async function createDocumentViaAPI(page: Page, content: any): Promise { + return await page.evaluate(async (docContent) => { + const repo = (window as any).testRepo; + if (!repo) throw new Error('Repository not available'); + const handle = await repo.createDocument(docContent); + return handle.documentId(); + }, content); +} + +async function getDocumentViaAPI(page: Page, docId: string): Promise { + return await page.evaluate(async (id) => { + const repo = (window as any).testRepo; + if (!repo) throw new Error('Repository not available'); + const handle = await repo.findDocument(id); + if (!handle) return null; + return handle.getDocument(); + }, docId); +} + +async function documentExistsViaAPI(page: Page, docId: string): Promise { + return await page.evaluate(async (id) => { + const repo = (window as any).testRepo; + if (!repo) return false; + try { + const handle = await repo.findDocument(id); + return handle !== null && handle !== undefined; + } catch (e) { + return false; + } + }, docId); +} + +test.describe('Samod WASM Integration Tests', () => { + let page: Page; + let server: TestServer; + + test.beforeEach(async ({ page: testPage }) => { + page = testPage; + + server = await createTestServer(); + const port = server.getPort(); + + // Navigate to the page with the server port + await page.goto(`/?wsPort=${port}`); + + // Wait for WASM initialization + await page.waitForSelector('.status:not(.connecting)', { timeout: 10000 }); + }); + + test.afterEach(async () => { + if (server) await server.stop(); + }); + + test('WASM module initializes successfully', async () => { + const status = await page.locator('#status').textContent(); + expect(status).toContain('WASM Ready'); + + const connectBtn = await page.locator('#connectBtn'); + expect(await connectBtn.isEnabled()).toBe(true); + + const logEntries = await page.locator('.log-entry.success').count(); + expect(logEntries).toBeGreaterThan(0); + }); + + test('can connect to WebSocket server', async () => { + await page.click('#connectBtn'); + + await page.waitForSelector('.status.connected', { timeout: 5000 }); + + const status = await page.locator('#status').textContent(); + expect(status).toBe('Connected'); + + const disconnectBtn = await page.locator('#disconnectBtn'); + expect(await disconnectBtn.isEnabled()).toBe(true); + }); + + test('can create and list documents', async () => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + await page.fill('#docTitle', 'Test Document'); + await page.fill('#docContent', 'This is test content'); + await page.click('#createDocBtn'); + + // Wait for document to be created + await page.waitForSelector('.document-item', { timeout: 5000 }); + + // Verify document appears in list + const docItem = await page.locator('.document-item').first(); + expect(await docItem.textContent()).toContain('Test Document'); + + // Verify metrics update + const docCount = await page.locator('#docCount').textContent(); + expect(docCount).toBe('1'); + }); + + test('can sync documents between clients', async ({ browser }) => { + // Client A: Connect and create document + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Client B: Open new browser context + const context = await browser.newContext(); + const pageB = await context.newPage(); + const port = server.getPort(); + await pageB.goto(`/?wsPort=${port}`); + await pageB.waitForSelector('.status:not(.connecting)'); + await pageB.click('#connectBtn'); + await pageB.waitForSelector('.status.connected'); + + // Create document in Client A + await page.fill('#docTitle', 'Cross-Client Sync Test'); + await page.fill('#docContent', 'This document should sync between clients'); + await page.click('#createDocBtn'); + await page.waitForSelector('.document-item'); + + // Verify document was created in Client A + const docItemA = await page.locator('.document-item').first(); + expect(await docItemA.textContent()).toContain('Cross-Client Sync Test'); + + // Client B: Poll sync button until document appears + const maxAttempts = 20; // 10 seconds total with 500ms intervals + let found = false; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + await pageB.click('#syncBtn'); + await pageB.waitForTimeout(500); + + const targetDocCount = await pageB + .locator('.document-item') + .filter({ hasText: 'Cross-Client Sync Test' }) + .count(); + + if (targetDocCount > 0) { + found = true; + break; + } + } + + expect(found).toBe(true); + + // Verify the document is visible + const docItems = await pageB + .locator('.document-item') + .filter({ hasText: 'Cross-Client Sync Test' }) + .first(); + await expect(docItems).toBeVisible(); + + // Verify both clients show the same document count + await page.click('#syncBtn'); + await pageB.click('#syncBtn'); + + const countA = await page.locator('#docCount').textContent(); + const countB = await pageB.locator('#docCount').textContent(); + expect(countA).toBe(countB); + + await context.close(); + }); + + test('handles disconnect and reconnect', async () => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Test client-side disconnection + await page.click('#disconnectBtn'); + await page.waitForSelector('.status.error'); + + let status = await page.locator('#status').textContent(); + expect(status).toBe('Disconnected'); + + // Reconnect + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + expect(await page.locator('#status').textContent()).toBe('Connected'); + }); + + test('handles server-side disconnection', async () => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Trigger server-side disconnection + const response = await page.request.post( + `http://localhost:${server.getPort()}/test/disconnect-all` + ); + expect(response.ok()).toBe(true); + + const result = await response.json(); + expect(result.disconnected).toBeGreaterThan(0); + + // Wait for client to detect the disconnection + await page.waitForSelector('.error', { timeout: 5000 }); + + const status = await page.locator('.error').allTextContents(); + expect(status.join(' ')).toContain('Detected server-side disconnection'); + + // Test reconnection after server-side disconnect + await page.waitForSelector('.status.connected'); + expect(await page.locator('#status').textContent()).toBe('Connected'); + }); + + test('handles errors gracefully', async () => { + // Try to create document without required fields + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Clear inputs and try to create + await page.fill('#docTitle', ''); + await page.fill('#docContent', ''); + await page.click('#createDocBtn'); + + // Should create with default values + await page.waitForSelector('.document-item'); + const docText = await page.locator('.document-item').textContent(); + expect(docText).toContain('Document 1'); + }); + + test('metrics update correctly', async () => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Create multiple documents + for (let i = 0; i < 3; i++) { + await page.fill('#docTitle', `Doc ${i + 1}`); + await page.click('#createDocBtn'); + await page.waitForTimeout(100); + } + + // Check metrics + const docCount = await page.locator('#docCount').textContent(); + expect(docCount).toBe('3'); + + const storageSize = await page.locator('#storageSize').textContent(); + expect(storageSize).toContain('KB'); + }); + + test('IndexedDB persistence across page reloads using direct API', async () => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + const testDocuments = [ + { + title: 'Simple Document', + content: 'Simple content with value: 123', + createdAt: new Date().toISOString(), + }, + { + title: 'Large Document', + content: 'Large document content: ' + 'x'.repeat(10000), + createdAt: new Date().toISOString(), + }, + ]; + + // Create documents using direct API and store their IDs + const documentIds: string[] = []; + for (const docData of testDocuments) { + const docId = await createDocumentViaAPI(page, docData); + documentIds.push(docId); + expect(docId).toBeTruthy(); + } + + // Wait for docs to persist + await page.waitForTimeout(500); + + // Verify all documents are created via API + for (let i = 0; i < documentIds.length; i++) { + const docContent = await getDocumentViaAPI(page, documentIds[i]); + expect(docContent).toBeTruthy(); + expect(docContent.title).toBe(testDocuments[i].title); + } + + // Clear server storage to ensure persistence is from IndexedDB + const clearResponse = await page.request.post( + `http://localhost:${server.getPort()}/test/clear-storage` + ); + expect(clearResponse.ok()).toBe(true); + + // Reload the page multiple times to test persistence + for (let reloadCount = 0; reloadCount < 3; reloadCount++) { + // Disconnect before reload to ensure no sync happens during reload + await page.click('#disconnectBtn'); + await page.waitForSelector('.status.error'); + + await page.reload(); + await page.waitForSelector('.status:not(.connecting)'); + + // Wait for discovery to complete during initialization + await page.waitForTimeout(1000); + + // Verify documents exist via API (must come from IndexedDB since server was cleared) + for (let i = 0; i < documentIds.length; i++) { + const exists = await documentExistsViaAPI(page, documentIds[i]); + expect(exists).toBe(true); + + const docContent = await getDocumentViaAPI(page, documentIds[i]); + expect(docContent).toBeTruthy(); + expect(docContent.title).toBe(testDocuments[i].title); + expect(docContent.content).toBe(testDocuments[i].content); + } + + // Now reconnect to server + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Verify documents are still accessible after reconnecting + for (let i = 0; i < documentIds.length; i++) { + const docContent = await getDocumentViaAPI(page, documentIds[i]); + expect(docContent).toBeTruthy(); + expect(docContent.title).toBe(testDocuments[i].title); + expect(docContent.content).toBe(testDocuments[i].content); + } + } + }); + + test('IndexedDB storage size calculations are accurate', async () => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Get initial metrics + let initialDocCount = await page.locator('#docCount').textContent(); + let initialStorageSize = await page.locator('#storageSize').textContent(); + expect(initialDocCount).toBe('0'); + expect(initialStorageSize).toBe('0 KB'); + + // Create a small document + await page.fill('#docTitle', 'Small Document'); + await page.fill('#docContent', 'Small amount of data'); + await page.click('#createDocBtn'); + await page.waitForSelector('.document-item'); + + // Check metrics after small document + await page.click('#syncBtn'); + let docCount = await page.locator('#docCount').textContent(); + let storageSize = await page.locator('#storageSize').textContent(); + expect(docCount).toBe('1'); + expect(storageSize).toContain('KB'); + expect(storageSize).not.toBe('0 KB'); + + // Create a medium document with more content + const mediumContent = Array.from( + { length: 10 }, + (_, i) => + `Field ${i}: This is field ${i} with some content that makes it longer` + ).join('\n'); + + await page.fill('#docTitle', 'Medium Document'); + await page.fill('#docContent', mediumContent); + await page.click('#createDocBtn'); + await page.waitForSelector('.document-item:nth-child(2)'); + + // Check metrics after medium document + await page.click('#syncBtn'); + docCount = await page.locator('#docCount').textContent(); + storageSize = await page.locator('#storageSize').textContent(); + expect(docCount).toBe('2'); + + // Storage should have increased + const mediumStorageValue = parseInt(storageSize!.replace(' KB', '')); + expect(mediumStorageValue).toBeGreaterThan(0); + + // Create a large document with substantial content + const largeContent = Array.from( + { length: 50 }, + (_, i) => + `Entry ${i}: This is a longer piece of text for entry ${i}. It contains more characters to simulate larger documents with substantial content that would take up more storage space in IndexedDB.` + ).join('\n'); + + await page.fill('#docTitle', 'Large Document'); + await page.fill('#docContent', largeContent); + await page.click('#createDocBtn'); + await page.waitForSelector('.document-item:nth-child(3)'); + + // Check final metrics + await page.click('#syncBtn'); + const finalDocCount = await page.locator('#docCount').textContent(); + const finalStorageSize = await page.locator('#storageSize').textContent(); + + expect(finalDocCount).toBe('3'); + expect(finalStorageSize).toContain('KB'); + + // Final storage should be significantly larger than medium + const finalStorageValue = parseInt(finalStorageSize!.replace(' KB', '')); + expect(finalStorageValue).toBeGreaterThan(mediumStorageValue); + + // Verify all documents are visible in the UI + const documentTitles = await page + .locator('.document-item strong') + .allTextContents(); + expect(documentTitles).toContain('Small Document'); + expect(documentTitles).toContain('Medium Document'); + expect(documentTitles).toContain('Large Document'); + }); + + test('IndexedDB persistence without server using direct API', async () => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Create test documents using direct API + const testDocuments = [ + { + title: 'API Document 1', + content: 'This should persist without server', + createdAt: new Date().toISOString(), + }, + { + title: 'API Document 2', + content: 'Another API document', + createdAt: new Date().toISOString(), + }, + ]; + + const documentIds: string[] = []; + for (const docData of testDocuments) { + const docId = await createDocumentViaAPI(page, docData); + documentIds.push(docId); + expect(docId).toBeTruthy(); + } + + // Verify documents exist via API + for (let i = 0; i < documentIds.length; i++) { + const docContent = await getDocumentViaAPI(page, documentIds[i]); + expect(docContent).toBeTruthy(); + expect(docContent.title).toBe(testDocuments[i].title); + expect(docContent.content).toBe(testDocuments[i].content); + } + + // Clear server storage to ensure documents can't come from there + const clearResponse = await page.request.post( + `http://localhost:${server.getPort()}/test/clear-storage` + ); + expect(clearResponse.ok()).toBe(true); + const clearResult = await clearResponse.json(); + expect(clearResult.cleared).toBe(true); + + // Disconnect from server + await page.click('#disconnectBtn'); + await page.waitForSelector('.status.error'); + + // Stop the server completely + await server.stop(); + + // Wait a moment for cleanup + await page.waitForTimeout(1000); + + // Reload the page with server down + await page.reload(); + await page.waitForSelector('.status:not(.connecting)', { timeout: 10000 }); + + // Wait for discovery to complete during initialization + await page.waitForTimeout(1000); + + // Verify documents still exist via API after reload (must come from IndexedDB) + for (let i = 0; i < documentIds.length; i++) { + const exists = await documentExistsViaAPI(page, documentIds[i]); + expect(exists).toBe(true); + + const docContent = await getDocumentViaAPI(page, documentIds[i]); + expect(docContent).toBeTruthy(); + expect(docContent.title).toBe(testDocuments[i].title); + expect(docContent.content).toBe(testDocuments[i].content); + } + + // Restart server for cleanup + server = await createTestServer(); + }); + + test('concurrent IndexedDB access from multiple tabs works', async ({ + browser, + }) => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Create a document in tab 1 + await page.fill('#docTitle', 'Tab 1 Document'); + await page.fill('#docContent', 'Created in first tab'); + await page.click('#createDocBtn'); + await page.waitForSelector('.document-item'); + + // Verify document was created in tab 1 + let tab1DocCount = await page.locator('.document-item').count(); + expect(tab1DocCount).toBe(1); + + const tab1DocTitle = await page + .locator('.document-item strong') + .textContent(); + expect(tab1DocTitle).toBe('Tab 1 Document'); + + // Open a second tab with the same application + const page2 = await browser.newPage(); + await page2.goto(page.url()); + await page2.waitForSelector('.status:not(.connecting)'); + await page2.click('#connectBtn'); + await page2.waitForSelector('.status.connected'); + + // Sync in tab 2 to load documents from IndexedDB + await page2.click('#syncBtn'); + await page2.waitForTimeout(1000); + + // Verify tab 2 can see the document created in tab 1 + const tab2InitialDocCount = await page2.locator('.document-item').count(); + expect(tab2InitialDocCount).toBe(1); + + const tab2SeesTab1Doc = await page2 + .locator('.document-item strong') + .textContent(); + expect(tab2SeesTab1Doc).toBe('Tab 1 Document'); + + // Create a document in tab 2 using UI + await page2.fill('#docTitle', 'Tab 2 Document'); + await page2.fill('#docContent', 'Created in second tab'); + await page2.click('#createDocBtn'); + await page2.waitForSelector('.document-item:nth-child(2)'); + + // Verify both documents are visible in tab 2 + const tab2FinalDocCount = await page2.locator('.document-item').count(); + expect(tab2FinalDocCount).toBe(2); + + const tab2DocTitles = await page2 + .locator('.document-item strong') + .allTextContents(); + expect(tab2DocTitles).toContain('Tab 1 Document'); + expect(tab2DocTitles).toContain('Tab 2 Document'); + + // Sync in tab 1 to see the document created in tab 2 + await page.click('#syncBtn'); + await page.waitForTimeout(1000); + + // Verify both documents are now visible in tab 1 + const tab1FinalDocCount = await page.locator('.document-item').count(); + expect(tab1FinalDocCount).toBe(2); + + const tab1DocTitles = await page + .locator('.document-item strong') + .allTextContents(); + expect(tab1DocTitles).toContain('Tab 1 Document'); + expect(tab1DocTitles).toContain('Tab 2 Document'); + + // Verify metrics are consistent across tabs + const tab1DocCountMetric = await page.locator('#docCount').textContent(); + const tab2DocCountMetric = await page2.locator('#docCount').textContent(); + + expect(tab1DocCountMetric).toBe('2'); + expect(tab2DocCountMetric).toBe('2'); + }); + + test('manual disconnect disables auto-reconnect', async () => { + await page.click('#connectBtn'); + await page.waitForSelector('.status.connected'); + + // Verify auto-reconnect is enabled initially + const connectedState = await page.evaluate(() => { + const app = (window as any).testApp; + return app.getConnectionState(); + }); + expect(connectedState.autoReconnect).toBe(true); + + // Manual disconnect + await page.click('#disconnectBtn'); + await page.waitForSelector('.status.error'); + + // Verify auto-reconnect is disabled after manual disconnect + const disconnectedState = await page.evaluate(() => { + const app = (window as any).testApp; + return app.getConnectionState(); + }); + + expect(disconnectedState.isConnected).toBe(false); + expect(disconnectedState.autoReconnect).toBe(false); + expect(disconnectedState.hasReconnectTimeout).toBe(false); + expect(disconnectedState.reconnectAttempts).toBe(0); + }); +}); diff --git a/wasm-tests/tests/server-helper.ts b/wasm-tests/tests/server-helper.ts new file mode 100644 index 0000000..348ba57 --- /dev/null +++ b/wasm-tests/tests/server-helper.ts @@ -0,0 +1,121 @@ +import { spawn, ChildProcess } from 'child_process'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import net from 'net'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +export class TestServer { + private process: ChildProcess | null = null; + private port: number = 0; + + async start(): Promise { + // Find an available port + this.port = await this.findAvailablePort(); + + // Start the server + return new Promise((resolve, reject) => { + const serverPath = join( + __dirname, + '../../samod/interop-test-server/server.js' + ); + + this.process = spawn('node', [serverPath, '--wasm', String(this.port)], { + stdio: ['ignore', 'pipe', 'pipe'], + env: { ...process.env, NODE_ENV: 'test' }, + }); + + let started = false; + + // Listen for startup message + this.process.stdout?.on('data', data => { + const output = data.toString(); + console.log(`[Server ${this.port}] ${output.trim()}`); + + if (output.includes('Listening on port') && !started) { + started = true; + resolve(this.port); + } + }); + + this.process.stderr?.on('data', data => { + console.error(`[Server ${this.port} Error] ${data.toString().trim()}`); + }); + + this.process.on('error', error => { + reject(new Error(`Failed to start server: ${error.message}`)); + }); + + this.process.on('exit', code => { + if (!started) { + reject(new Error(`Server exited with code ${code} before starting`)); + } + }); + + // Timeout if server doesn't start + setTimeout(() => { + if (!started) { + this.stop(); + reject(new Error('Server failed to start within timeout')); + } + }, 10000); + }); + } + + async stop(): Promise { + if (this.process) { + return new Promise(resolve => { + this.process!.once('exit', () => { + this.process = null; + resolve(); + }); + + this.process!.kill('SIGTERM'); + + // Force kill after timeout + setTimeout(() => { + if (this.process) { + this.process.kill('SIGKILL'); + } + }, 5000); + }); + } + } + + getPort(): number { + return this.port; + } + + getUrl(): string { + return `http://localhost:${this.port}`; + } + + getWsUrl(): string { + return `ws://localhost:${this.port}`; + } + + private async findAvailablePort(): Promise { + return new Promise((resolve, reject) => { + const server = net.createServer(); + + server.listen(0, () => { + const address = server.address(); + if (address && typeof address !== 'string') { + const port = address.port; + server.close(() => resolve(port)); + } else { + reject(new Error('Failed to get port')); + } + }); + + server.on('error', reject); + }); + } +} + +// Helper function to create and start a server +export async function createTestServer(): Promise { + const server = new TestServer(); + await server.start(); + return server; +} diff --git a/wasm-tests/tsconfig.json b/wasm-tests/tsconfig.json new file mode 100644 index 0000000..853ddc4 --- /dev/null +++ b/wasm-tests/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "moduleResolution": "node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "allowJs": true, + "checkJs": false, + "declaration": true, + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "pkg"] +} \ No newline at end of file diff --git a/wasm-tests/vite.config.js b/wasm-tests/vite.config.js new file mode 100644 index 0000000..443527d --- /dev/null +++ b/wasm-tests/vite.config.js @@ -0,0 +1,18 @@ +import { defineConfig } from 'vite'; +import path from 'path'; + +export default defineConfig({ + root: path.resolve(__dirname, 'src'), + server: { + port: 3000, + proxy: { + '/ws': { + target: 'ws://localhost:3001', + ws: true, + }, + }, + }, + optimizeDeps: { + exclude: ['samod_wasm'], + }, +}); \ No newline at end of file