diff --git a/.gitignore b/.gitignore index 384c7ac9..f7c55395 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # Build artifacts target/ node_modules/ +dist/ # Dart workspace artifacts .dart_tool diff --git a/Cargo.lock b/Cargo.lock index d3329fc9..da7de721 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,16 @@ version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ - "gimli", + "gimli 0.31.1", +] + +[[package]] +name = "addr2line" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9acbfca36652500c911ddb767ed433e3ed99b032b5d935be73c6923662db1d43" +dependencies = [ + "gimli 0.32.2", ] [[package]] @@ -115,6 +124,12 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "ambient-authority" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -682,11 +697,11 @@ version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ - "addr2line", + "addr2line 0.24.2", "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.36.7", "rustc-demangle", "windows-targets 0.52.6", ] @@ -976,6 +991,84 @@ dependencies = [ "serde", ] +[[package]] +name = "cap-fs-ext" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c" +dependencies = [ + "cap-primitives", + "cap-std", + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "cap-net-ext" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c" +dependencies = [ + "cap-primitives", + "cap-std", + "rustix 1.0.8", + "smallvec", +] + +[[package]] +name = "cap-primitives" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a" +dependencies = [ + "ambient-authority", + "fs-set-times", + "io-extras", + "io-lifetimes", + "ipnet", + "maybe-owned", + "rustix 1.0.8", + "rustix-linux-procfs", + "windows-sys 0.59.0", + "winx", +] + +[[package]] +name = "cap-rand" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40" +dependencies = [ + "ambient-authority", + "rand 0.8.5", +] + +[[package]] +name = "cap-std" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189" +dependencies = [ + "cap-primitives", + "io-extras", + "io-lifetimes", + "rustix 1.0.8", +] + +[[package]] +name = "cap-time-ext" +version = "3.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b" +dependencies = [ + "ambient-authority", + "cap-primitives", + "iana-time-zone", + "once_cell", + "rustix 1.0.8", + "winx", +] + [[package]] name = "capacity_builder" version = "0.5.0" @@ -1225,6 +1318,15 @@ dependencies = [ "cc", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.16", +] + [[package]] name = "colorchoice" version = "1.0.4" @@ -1320,6 +1422,15 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpp_demangle" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96e58d342ad113c2b878f16d5d034c03be492ae460cdbc02b7f0f2284d310c7d" +dependencies = [ + "cfg-if", +] + [[package]] name = "cpufeatures" version = "0.2.17" @@ -1329,6 +1440,144 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-assembler-x64" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0920ef6863433fa28ece7e53925be4cd39a913adba2dc3738f4edd182f76d168" +dependencies = [ + "cranelift-assembler-x64-meta", +] + +[[package]] +name = "cranelift-assembler-x64-meta" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8990a217e2529a378af1daf4f8afa889f928f07ebbde6ae2f058ae60e40e2c20" +dependencies = [ + "cranelift-srcgen", +] + +[[package]] +name = "cranelift-bforest" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62225596b687f69a42c038485a28369badc186cb7c74bd9436eeec9f539011b1" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c23914fc4062558650a6f0d8c1846c97b541215a291fdeabc85f68bdc9bbcca3" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a238b2f7e7ec077eb170145fa15fd8b3d0f36cc83d8e354e29ca550f339ca7" +dependencies = [ + "bumpalo", + "cranelift-assembler-x64", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.32.2", + "hashbrown 0.15.5", + "log", + "pulley-interpreter", + "regalloc2", + "rustc-hash 2.1.1", + "serde", + "smallvec", + "target-lexicon", + "wasmtime-internal-math", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9315ddcc2512513a9d66455ec89bb70ae5498cb472f5ed990230536f4cd5c011" +dependencies = [ + "cranelift-assembler-x64-meta", + "cranelift-codegen-shared", + "cranelift-srcgen", + "heck", + "pulley-interpreter", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6acea40ef860f28cb36eaad479e26556c1e538b0a66fc44598cf1b1689393d" + +[[package]] +name = "cranelift-control" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b2af895da90761cfda4a4445960554fcec971e637882eda5a87337d993fe1b9" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e8c542c856feb50d504e4fc0526b3db3a514f882a9f68f956164531517828ab" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9996dd9c20929c03360fe0c4edf3594c0cbb94525bdbfa04b6bb639ec14573c7" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928b8dccad51b9e0ffe54accbd617da900239439b13d48f0f122ab61105ca6ad" + +[[package]] +name = "cranelift-native" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f75ef0a6a2efed3a2a14812318e28dc82c214eab5399c13d70878e2f88947b5" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-srcgen" +version = "0.123.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673bd6d1c83cb41d60afb140a1474ef6caf1a3e02f3820fc522aefbc93ac67d6" + [[package]] name = "crc32fast" version = "1.5.0" @@ -1730,7 +1979,7 @@ dependencies = [ "serde_json", "thiserror 2.0.16", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-socks", "tokio-util", "tokio-vsock", @@ -2032,7 +2281,7 @@ dependencies = [ "deno_core", "deno_error", "deno_native_certs", - "rustls", + "rustls 0.23.28", "rustls-pemfile", "rustls-tokio-stream", "rustls-webpki 0.102.8", @@ -2177,6 +2426,27 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -2338,6 +2608,18 @@ dependencies = [ "serde", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -2654,6 +2936,17 @@ dependencies = [ "syn", ] +[[package]] +name = "fs-set-times" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" +dependencies = [ + "io-lifetimes", + "rustix 1.0.8", + "windows-sys 0.59.0", +] + [[package]] name = "fs3" version = "0.5.0" @@ -2789,6 +3082,28 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "fxprof-processed-profile" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27d12c0aed7f1e24276a241aadc4cb8ea9f83000f34bc062b7cc2d51e3b0fabd" +dependencies = [ + "bitflags", + "debugid", + "fxhash", + "serde", + "serde_json", +] + [[package]] name = "generator" version = "0.8.7" @@ -2857,6 +3172,17 @@ version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +[[package]] +name = "gimli" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6298e594375a7fead9efd5568f0a46e6a154fb6a9bdcbe3c06946ffd81a5f6" +dependencies = [ + "fallible-iterator", + "indexmap 2.11.0", + "stable_deref_trait", +] + [[package]] name = "glob" version = "0.3.3" @@ -2957,6 +3283,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "foldhash", + "serde", ] [[package]] @@ -3164,11 +3491,11 @@ dependencies = [ "http", "hyper", "hyper-util", - "rustls", + "rustls 0.23.28", "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower-service", "webpki-roots 1.0.2", ] @@ -3333,6 +3660,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + [[package]] name = "idna" version = "1.1.0" @@ -3406,6 +3739,22 @@ dependencies = [ "generic-array", ] +[[package]] +name = "io-extras" +version = "0.18.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" +dependencies = [ + "io-lifetimes", + "windows-sys 0.59.0", +] + +[[package]] +name = "io-lifetimes" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" + [[package]] name = "io-uring" version = "0.7.10" @@ -3501,6 +3850,26 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "ittapi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b996fe614c41395cdaedf3cf408a9534851090959d90d54a535f675550b64b1" +dependencies = [ + "anyhow", + "ittapi-sys", + "log", +] + +[[package]] +name = "ittapi-sys" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52f5385394064fa2c886205dba02598013ce83d3e92d33dbdc0c52fe0e7bf4fc" +dependencies = [ + "cc", +] + [[package]] name = "jiff" version = "0.2.15" @@ -3630,6 +3999,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "lettre" version = "0.11.18" @@ -3651,10 +4026,10 @@ dependencies = [ "nom 8.0.0", "percent-encoding", "quoted_printable", - "rustls", + "rustls 0.23.28", "socket2 0.6.0", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "url", "webpki-roots 1.0.2", ] @@ -3813,6 +4188,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.2.0" @@ -3847,6 +4231,12 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "maybe-owned" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" + [[package]] name = "maybe_path" version = "0.1.3" @@ -3872,6 +4262,15 @@ version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +[[package]] +name = "memfd" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" +dependencies = [ + "rustix 1.0.8", +] + [[package]] name = "memoffset" version = "0.9.1" @@ -4245,6 +4644,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "crc32fast", + "hashbrown 0.15.5", + "indexmap 2.11.0", + "memchr", +] + [[package]] name = "object_store" version = "0.12.3" @@ -4798,6 +5209,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.3" @@ -5011,6 +5434,29 @@ dependencies = [ "unicase", ] +[[package]] +name = "pulley-interpreter" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e2d31146038fd9e62bfa331db057aca325d5ca10451a9fe341356cead7da53" +dependencies = [ + "cranelift-bitset", + "log", + "pulley-macros", + "wasmtime-internal-math", +] + +[[package]] +name = "pulley-macros" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efb9fdafaca625f9ea8cfa793364ea1bdd32d306cff18f166b00ddaa61ecbb27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "quick-xml" version = "0.38.3" @@ -5033,7 +5479,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.28", "socket2 0.6.0", "thiserror 2.0.16", "tokio", @@ -5055,7 +5501,7 @@ dependencies = [ "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls", + "rustls 0.23.28", "rustls-pki-types", "slab", "thiserror 2.0.16", @@ -5236,6 +5682,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "ref-cast" version = "1.0.24" @@ -5270,6 +5727,20 @@ dependencies = [ "serde_json", ] +[[package]] +name = "regalloc2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5216b1837de2149f8bc8e6d5f88a9326b63b8c836ed58ce4a0a29ec736a59734" +dependencies = [ + "allocator-api2", + "bumpalo", + "hashbrown 0.15.5", + "log", + "rustc-hash 2.1.1", + "smallvec", +] + [[package]] name = "regex" version = "1.11.2" @@ -5328,7 +5799,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls", + "rustls 0.23.28", "rustls-native-certs 0.8.1", "rustls-pemfile", "rustls-pki-types", @@ -5339,7 +5810,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tokio-rustls", + "tokio-rustls 0.26.2", "tokio-util", "tower-service", "url", @@ -5536,6 +6007,30 @@ dependencies = [ "windows-sys 0.60.2", ] +[[package]] +name = "rustix-linux-procfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" +dependencies = [ + "once_cell", + "rustix 1.0.8", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle", + "zeroize", +] + [[package]] name = "rustls" version = "0.23.28" @@ -5604,7 +6099,7 @@ checksum = "0560d12c0d8c672f849197de91b9ee61f5bf9aa024c97aaeeb112ec2f6c347fd" dependencies = [ "derive-io", "futures", - "rustls", + "rustls 0.23.28", "socket2 0.5.10", "tokio", ] @@ -5695,7 +6190,7 @@ dependencies = [ "once_cell", "paste", "reqwest", - "rustls", + "rustls 0.23.28", "rustyline", "serde", "thiserror 2.0.16", @@ -5946,6 +6441,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -6125,6 +6629,9 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] [[package]] name = "smartstring" @@ -6785,6 +7292,22 @@ dependencies = [ "libc", ] +[[package]] +name = "system-interface" +version = "0.27.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" +dependencies = [ + "bitflags", + "cap-fs-ext", + "cap-std", + "fd-lock", + "io-lifetimes", + "rustix 0.38.44", + "windows-sys 0.59.0", + "winx", +] + [[package]] name = "tagptr" version = "0.2.0" @@ -6797,6 +7320,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a" + [[package]] name = "temp-dir" version = "0.1.16" @@ -6989,13 +7518,24 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls", + "rustls 0.23.28", "tokio", ] @@ -7048,6 +7588,47 @@ dependencies = [ "vsock", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.11.0", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow 0.7.13", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "tonic" version = "0.12.3" @@ -7305,7 +7886,7 @@ dependencies = [ "temp-dir", "thiserror 2.0.16", "tokio", - "tokio-rustls", + "tokio-rustls 0.26.2", "tower 0.5.2", "tower-cookies", "tower-http", @@ -7320,6 +7901,8 @@ dependencies = [ "trailbase-refinery", "trailbase-schema", "trailbase-sqlite", + "trailbase-wasm-common", + "trailbase-wasm-runtime-host", "ts-rs", "url", "utoipa", @@ -7526,6 +8109,55 @@ dependencies = [ "uuid", ] +[[package]] +name = "trailbase-wasi-keyvalue" +version = "0.1.0" +dependencies = [ + "anyhow", + "parking_lot", + "tokio", + "wasmtime", + "wasmtime-wasi", +] + +[[package]] +name = "trailbase-wasm-common" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "ts-rs", +] + +[[package]] +name = "trailbase-wasm-runtime-host" +version = "0.1.0" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body-util", + "hyper", + "kanal", + "log", + "parking_lot", + "rusqlite", + "self_cell", + "serde", + "serde_json", + "thiserror 2.0.16", + "tokio", + "tracing", + "trailbase-schema", + "trailbase-sqlite", + "trailbase-wasi-keyvalue", + "trailbase-wasm-common", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-http", + "wasmtime-wasi-io", +] + [[package]] name = "triomphe" version = "0.1.14" @@ -7670,6 +8302,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "unicode_categories" version = "0.1.1" @@ -7988,6 +8626,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "724fccfd4f3c24b7e589d333fc0429c68042897a7e8a5f8694f31792471841e7" +dependencies = [ + "leb128fmt", + "wasmparser 0.236.1", +] + +[[package]] +name = "wasm-encoder" +version = "0.238.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50d48c31c615f77679b61c607b8151378a5d03159616bf3d17e8e2005afdaf5" +dependencies = [ + "leb128fmt", + "wasmparser 0.238.1", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -8011,6 +8669,398 @@ dependencies = [ "thiserror 2.0.16", ] +[[package]] +name = "wasmparser" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9b1e81f3eb254cf7404a82cee6926a4a3ccc5aad80cc3d43608a070c67aa1d7" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.11.0", + "semver 1.0.26", + "serde", +] + +[[package]] +name = "wasmparser" +version = "0.238.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fa99c8328024423875ae4a55345cfde8f0371327fb2d0f33b0f52a06fc44408" +dependencies = [ + "bitflags", + "indexmap 2.11.0", + "semver 1.0.26", +] + +[[package]] +name = "wasmprinter" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2df225df06a6df15b46e3f73ca066ff92c2e023670969f7d50ce7d5e695abbb1" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.236.1", +] + +[[package]] +name = "wasmtime" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3e1fab634681494213138ea3a18e958e5ea99da13a4a01a4b870d51a41680b" +dependencies = [ + "addr2line 0.25.0", + "anyhow", + "async-trait", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "encoding_rs", + "fxprof-processed-profile", + "gimli 0.32.2", + "hashbrown 0.15.5", + "indexmap 2.11.0", + "ittapi", + "libc", + "log", + "mach2", + "memfd", + "object 0.37.3", + "once_cell", + "postcard", + "pulley-interpreter", + "rayon", + "rustix 1.0.8", + "semver 1.0.26", + "serde", + "serde_derive", + "serde_json", + "smallvec", + "target-lexicon", + "wasm-encoder 0.236.1", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-asm-macros", + "wasmtime-internal-cache", + "wasmtime-internal-component-macro", + "wasmtime-internal-component-util", + "wasmtime-internal-cranelift", + "wasmtime-internal-fiber", + "wasmtime-internal-jit-debug", + "wasmtime-internal-jit-icache-coherence", + "wasmtime-internal-math", + "wasmtime-internal-slab", + "wasmtime-internal-unwinder", + "wasmtime-internal-versioned-export-macros", + "wasmtime-internal-winch", + "wat", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-environ" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6750e519977953a018fe994aada7e02510aea4babb03310aa5f5b4145b6e6577" +dependencies = [ + "anyhow", + "cpp_demangle", + "cranelift-bitset", + "cranelift-entity", + "gimli 0.32.2", + "indexmap 2.11.0", + "log", + "object 0.37.3", + "postcard", + "rustc-demangle", + "semver 1.0.26", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.236.1", + "wasmparser 0.236.1", + "wasmprinter", + "wasmtime-internal-component-util", +] + +[[package]] +name = "wasmtime-internal-asm-macros" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdbf38adac6e81d5c0326e8fd25f80450e3038f2fc103afd3c5cc8b83d5dd78b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-internal-cache" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0c9085d8c04cc294612d743e2f355382b39250de4bd20bf4b0b0b7c0ae7067a" +dependencies = [ + "anyhow", + "base64", + "directories-next", + "log", + "postcard", + "rustix 1.0.8", + "serde", + "serde_derive", + "sha2", + "toml", + "windows-sys 0.60.2", + "zstd", +] + +[[package]] +name = "wasmtime-internal-component-macro" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26a578a474e3b7ddce063cd169ced292b5185013341457522891b10e989aa42a" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn", + "wasmtime-internal-component-util", + "wasmtime-internal-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-internal-component-util" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc23d46ec1b1cd42b6f73205eb80498ed94b47098ec53456c0b18299405b158" + +[[package]] +name = "wasmtime-internal-cranelift" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85b8ba128525bff91b89ac8a97755136a4fb0fb59df5ffb7539dd646455d441" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli 0.32.2", + "itertools 0.14.0", + "log", + "object 0.37.3", + "pulley-interpreter", + "smallvec", + "target-lexicon", + "thiserror 2.0.16", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-math", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-fiber" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c566f5137de1f55339df8a236a5ec89698b466a3d33f9cc07823a58a3f85e16" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "libc", + "rustix 1.0.8", + "wasmtime-internal-asm-macros", + "wasmtime-internal-versioned-export-macros", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-jit-debug" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e03f0b11f8fe4d456feac11e7e9dc6f02ddb34d4f6a1912775dbc63c5bdd5670" +dependencies = [ + "cc", + "object 0.37.3", + "rustix 1.0.8", + "wasmtime-internal-versioned-export-macros", +] + +[[package]] +name = "wasmtime-internal-jit-icache-coherence" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71aeb74f9b3fd9225319c723e59832a77a674b0c899ba9795f9b2130a6d1b167" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-internal-math" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31d5dad8a609c6cc47a5f265f13b52e347e893450a69641af082b8a276043fa7" +dependencies = [ + "libm", +] + +[[package]] +name = "wasmtime-internal-slab" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d152a7b875d62e395bfe0ae7d12e7b47cd332eb380353cce3eb831f9843731d" + +[[package]] +name = "wasmtime-internal-unwinder" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aaacc0fea00293f7af7e6c25cef74b7d213ebbe7560c86305eec15fc318fab8" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "log", + "object 0.37.3", +] + +[[package]] +name = "wasmtime-internal-versioned-export-macros" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61c7f75326434944cc5f3b75409a063fa37e537f6247f00f0f733679f0be406" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasmtime-internal-winch" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cfbaa87e1ac4972bb096c9cb1800fedc113e36332cc4bc2c96a2ef1d7c5e750" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli 0.32.2", + "object 0.37.3", + "target-lexicon", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "winch-codegen", +] + +[[package]] +name = "wasmtime-internal-wit-bindgen" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "169042d58002f16da149ab7d608b71164411abd1fc5140f48f4c200b44bb5565" +dependencies = [ + "anyhow", + "bitflags", + "heck", + "indexmap 2.11.0", + "wit-parser", +] + +[[package]] +name = "wasmtime-wasi" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9049a5fedcd24fa0f665ba7c17c4445c1a547536a9560d960e15bee2d8428d0" +dependencies = [ + "anyhow", + "async-trait", + "bitflags", + "bytes", + "cap-fs-ext", + "cap-net-ext", + "cap-rand", + "cap-std", + "cap-time-ext", + "fs-set-times", + "futures", + "io-extras", + "io-lifetimes", + "rustix 1.0.8", + "system-interface", + "thiserror 2.0.16", + "tokio", + "tracing", + "url", + "wasmtime", + "wasmtime-wasi-io", + "windows-sys 0.60.2", +] + +[[package]] +name = "wasmtime-wasi-http" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1eea0b00539f0a3defce68bddb746736debea787e2f3a67cb562df977eb65a7" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "futures", + "http", + "http-body", + "http-body-util", + "hyper", + "rustls 0.22.4", + "tokio", + "tokio-rustls 0.25.0", + "tracing", + "wasmtime", + "wasmtime-wasi", + "wasmtime-wasi-io", + "webpki-roots 0.26.11", +] + +[[package]] +name = "wasmtime-wasi-io" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d62156d8695d80df8e85baeb56379b3ba6b6bf5996671594724c24d40b67825f" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "futures", + "wasmtime", +] + +[[package]] +name = "wast" +version = "238.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0a564e7eab2abb8920c1302b90eb2c98a15efbbe30fc060d4e2d88483aa23fe" +dependencies = [ + "bumpalo", + "leb128fmt", + "memchr", + "unicode-width 0.2.1", + "wasm-encoder 0.238.1", +] + +[[package]] +name = "wat" +version = "1.238.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eb84e6ac2997025f80482266fdc9f60fa28ba791b674bfd33855e77fe867631" +dependencies = [ + "wast", +] + [[package]] name = "web-sys" version = "0.3.78" @@ -8128,6 +9178,26 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "winch-codegen" +version = "36.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e615fe205d7d4c9aa62217862f2e0969d00b9b0843af0b1b8181adaea3cfef3" +dependencies = [ + "anyhow", + "cranelift-assembler-x64", + "cranelift-codegen", + "gimli 0.32.2", + "regalloc2", + "smallvec", + "target-lexicon", + "thiserror 2.0.16", + "wasmparser 0.236.1", + "wasmtime-environ", + "wasmtime-internal-cranelift", + "wasmtime-internal-math", +] + [[package]] name = "windows" version = "0.61.3" @@ -8540,12 +9610,40 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "winx" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" +dependencies = [ + "bitflags", + "windows-sys 0.59.0", +] + [[package]] name = "wit-bindgen" version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36" +[[package]] +name = "wit-parser" +version = "0.236.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e4833a20cd6e85d6abfea0e63a399472d6f88c6262957c17f546879a80ba15" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.11.0", + "log", + "semver 1.0.26", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.236.1", +] + [[package]] name = "writeable" version = "0.6.1" @@ -8737,3 +9835,31 @@ dependencies = [ "log", "simd-adler32", ] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index c50a26a3..c508eec0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,9 @@ members = [ "crates/schema", "crates/sqlean", "crates/sqlite", + "crates/wasi-keyvalue", + "crates/wasm-runtime-host", + "crates/wasm-runtime-common", "docs/examples/record_api_rs", "examples/custom-binary", ] @@ -25,8 +28,13 @@ default-members = [ "crates/extension", "crates/js-runtime", "crates/qs", + "crates/refinery", "crates/schema", + "crates/sqlean", "crates/sqlite", + "crates/wasi-keyvalue", + "crates/wasm-runtime-host", + "crates/wasm-runtime-common", ] # https://doc.rust-lang.org/cargo/reference/profiles.html @@ -66,15 +74,24 @@ rust-embed = { version = "8.4.0", default-features = false, features = ["mime-gu tokio = { version = "^1.38.0", default-features = false, features = ["macros", "net", "rt-multi-thread", "fs", "signal", "time", "sync"] } tracing = { version = "0.1.40", default-features = false } tracing-subscriber = { version = "0.3.18", default-features = false, features = ["smallvec", "std", "fmt", "json"] } +trailbase = { path = "crates/core", version = "0.2.0" } trailbase-assets = { path = "crates/assets", version = "0.2.0" } trailbase-build = { path = "crates/build", version = "0.1.1" } trailbase-client = { path = "crates/client", version = "0.5.0" } -trailbase-sqlean = { path = "crates/sqlean", version = "0.0.3" } trailbase-extension = { path = "crates/extension", version = "0.3.0" } trailbase-js = { path = "crates/js-runtime", version = "0.2.0" } trailbase-qs = { path = "crates/qs", version = "0.1.0" } trailbase-refinery = { path = "crates/refinery", version = "0.1.0" } trailbase-schema = { path = "crates/schema", version = "0.1.0" } +trailbase-sqlean = { path = "crates/sqlean", version = "0.0.3" } trailbase-sqlite = { path = "crates/sqlite", version = "0.3.0" } -trailbase = { path = "crates/core", version = "0.2.0" } +trailbase-wasi-keyvalue = { path = "crates/wasi-keyvalue", version = "0.1.0" } +trailbase-wasm = { path = "crates/wasm-runtime-guest", version = "0.1.0" } +trailbase-wasm-common = { path = "crates/wasm-runtime-common", version = "0.1.0" } +trailbase-wasm-runtime-host = { path = "crates/wasm-runtime-host", version = "0.1.0" } +ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl"] } uuid = { version = "1", default-features = false, features = ["std", "v4", "v7", "serde"] } +wasmtime = "36.0.1" +wasmtime-wasi = { version = "36.0.1", default-features = false, features = [] } +wasmtime-wasi-http = "36.0.1" +wasmtime-wasi-io = "36.0.1" diff --git a/client/dart/test/trailbase_test.dart b/client/dart/test/trailbase_test.dart index 7dffa0ee..1896c5fe 100644 --- a/client/dart/test/trailbase_test.dart +++ b/client/dart/test/trailbase_test.dart @@ -174,7 +174,7 @@ Future initTrailBase() async { 'run', '--address=${address}', // We want at least some parallelism to experience isolate-local state. - '--js-runtime-threads=2', + '--runtime-threads=2', ]); final dio = Dio(); diff --git a/client/dotnet/test/ClientTest.cs b/client/dotnet/test/ClientTest.cs index 5adf2817..a4ae6ca6 100644 --- a/client/dotnet/test/ClientTest.cs +++ b/client/dotnet/test/ClientTest.cs @@ -58,7 +58,7 @@ public class ClientTestFixture : IDisposable { process = new Process(); process.StartInfo.WorkingDirectory = projectDirectory; process.StartInfo.FileName = "cargo"; - process.StartInfo.Arguments = $"run -- --data-dir ../../testfixture run -a {address} --js-runtime-threads 2"; + process.StartInfo.Arguments = $"run -- --data-dir ../../testfixture run -a {address} --runtime-threads 2"; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.Start(); diff --git a/client/go/trailbase/client_test.go b/client/go/trailbase/client_test.go index 6de61c01..1746ad43 100644 --- a/client/go/trailbase/client_test.go +++ b/client/go/trailbase/client_test.go @@ -52,7 +52,7 @@ func startTrailBase() (*exec.Cmd, error) { fmt.Sprint("--data-dir=", traildepot), "run", fmt.Sprintf("--address=127.0.0.1:%d", PORT), - "--js-runtime-threads=2", + "--runtime-threads=2", } cmd := buildCommand("cargo", cwd, args...) cmd.Start() diff --git a/client/python/tests/test_client.py b/client/python/tests/test_client.py index 1809ae81..036d6762 100644 --- a/client/python/tests/test_client.py +++ b/client/python/tests/test_client.py @@ -38,7 +38,7 @@ class TrailBaseFixture: "run", "-a", address, - "--js-runtime-threads", + "--runtime-threads", "1", ] ) diff --git a/client/swift/trailbase/Tests/TrailBaseTests/TrailBaseTests.swift b/client/swift/trailbase/Tests/TrailBaseTests/TrailBaseTests.swift index 0e225952..cd681b0a 100644 --- a/client/swift/trailbase/Tests/TrailBaseTests/TrailBaseTests.swift +++ b/client/swift/trailbase/Tests/TrailBaseTests/TrailBaseTests.swift @@ -56,7 +56,7 @@ func startTrailBase() async throws -> ProcessIdentifier { "--data-dir=\(depotPath)", "run", "--address=127.0.0.1:\(PORT)", - "--js-runtime-threads=2", + "--runtime-threads=2", ] let process = try Subprocess.runDetached( diff --git a/client/testfixture/scripts/index.ts b/client/testfixture/scripts.delme/index.ts similarity index 87% rename from client/testfixture/scripts/index.ts rename to client/testfixture/scripts.delme/index.ts index 22e22530..b72e2c30 100644 --- a/client/testfixture/scripts/index.ts +++ b/client/testfixture/scripts.delme/index.ts @@ -117,7 +117,18 @@ addRoute( return await response.text(); } - throw new HttpError(StatusCodes.BAD_REQUEST, "Missing ?url param"); + throw new HttpError(StatusCodes.BAD_REQUEST, `Missing ?url param: ${req.params}`); + }), +); + +addRoute( + "GET", + "/fibonacci", + stringHandler((req: StringRequestType) => { + const uri: ParsedPath = parsePath(req.uri); + const n = uri.query.get("n"); + + return fibonacci(n ? parseInt(n) : 40).toString(); }), ); @@ -160,17 +171,28 @@ class Completer { const completer = new Completer(); -addCronCallback("JS-registered Job", "@hourly", async () => { - console.info("JS-registered cron job reporting for duty 🚀"); -}); - addPeriodicCallback(100, (cancel) => { completer.complete("resolved"); cancel(); }); +addCronCallback("JS-registered Job", "@hourly", async () => { + console.info("JS-registered cron job reporting for duty 🚀"); +}); + addRoute( "GET", "/await", stringHandler(async (_req) => await completer.promise), ); + +function fibonacci(num: number): number { + switch (num) { + case 0: + return 0; + case 1: + return 1; + default: + return fibonacci(num - 1) + fibonacci(num - 2); + } +} diff --git a/client/testfixture/wasm/wasm_rust_guest_testfixture.wasm b/client/testfixture/wasm/wasm_rust_guest_testfixture.wasm new file mode 100644 index 00000000..bae02733 Binary files /dev/null and b/client/testfixture/wasm/wasm_rust_guest_testfixture.wasm differ diff --git a/crates/assets/js/bindings/HttpContext.ts b/crates/assets/js/bindings/HttpContext.ts new file mode 100644 index 00000000..341baa91 --- /dev/null +++ b/crates/assets/js/bindings/HttpContext.ts @@ -0,0 +1,5 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HttpContextKind } from "./HttpContextKind"; +import type { HttpContextUser } from "./HttpContextUser"; + +export type HttpContext = { kind: HttpContextKind, registered_path: string, path_params: Array<[string, string]>, user: HttpContextUser | null, }; diff --git a/crates/assets/js/bindings/HttpContextKind.ts b/crates/assets/js/bindings/HttpContextKind.ts new file mode 100644 index 00000000..cd255e69 --- /dev/null +++ b/crates/assets/js/bindings/HttpContextKind.ts @@ -0,0 +1,3 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HttpContextKind = "Http" | "Job"; diff --git a/crates/assets/js/bindings/HttpContextUser.ts b/crates/assets/js/bindings/HttpContextUser.ts new file mode 100644 index 00000000..97985ed2 --- /dev/null +++ b/crates/assets/js/bindings/HttpContextUser.ts @@ -0,0 +1,15 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type HttpContextUser = { +/** + * Url-safe Base64 encoded id of the current user. + */ +id: string, +/** + * E-mail of the current user. + */ +email: string, +/** + * The "expected" CSRF token as included in the auth token claims [User] was constructed from. + */ +csrf_token: string, }; diff --git a/crates/assets/js/bindings/SqliteRequest.ts b/crates/assets/js/bindings/SqliteRequest.ts new file mode 100644 index 00000000..22ac206a --- /dev/null +++ b/crates/assets/js/bindings/SqliteRequest.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +export type SqliteRequest = { query: string, params: Array, }; diff --git a/crates/assets/js/bindings/SqliteResponse.ts b/crates/assets/js/bindings/SqliteResponse.ts new file mode 100644 index 00000000..37e7d8c0 --- /dev/null +++ b/crates/assets/js/bindings/SqliteResponse.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "./serde_json/JsonValue"; + +export type SqliteResponse = { "Query": { rows: Array>, } } | { "Execute": { rows_affected: number, } } | { "Error": string } | "TxBegin" | "TxCommit" | "TxRollback"; diff --git a/crates/assets/js/client/tests/integration_test_runner.ts b/crates/assets/js/client/tests/integration_test_runner.ts index 8973f045..5c1189f3 100644 --- a/crates/assets/js/client/tests/integration_test_runner.ts +++ b/crates/assets/js/client/tests/integration_test_runner.ts @@ -28,7 +28,7 @@ async function initTrailBase(): Promise<{ subprocess: Subprocess }> { cwd: root, stdout: process.stdout, stderr: process.stdout, - })`cargo run -- --data-dir client/testfixture --public-url http://${ADDRESS} run -a ${ADDRESS} --js-runtime-threads 1`; + })`cargo run -- --data-dir client/testfixture --public-url http://${ADDRESS} run -a ${ADDRESS} --runtime-threads 1`; for (let i = 0; i < 100; ++i) { if ((subprocess.exitCode ?? 0) > 0) { diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 02088c7b..d0c95789 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -93,6 +93,10 @@ pub struct ServerArgs { #[arg(long, env)] pub public_dir: Option, + /// Optional path to sandboxed FS root for WASM runtime. + #[arg(long, env)] + pub runtime_root_fs: Option, + /// Optional path to MaxmindDB geoip database. Can be used to map logged IPs to a geo location. #[arg(long, env)] pub geoip_db_path: Option, @@ -119,7 +123,7 @@ pub struct ServerArgs { /// Number of JavaScript isolates/workers to start (Default: #cpus). #[arg(long, env)] - pub js_runtime_threads: Option, + pub runtime_threads: Option, } #[derive(Args, Clone, Debug)] diff --git a/crates/cli/src/bin/trail.rs b/crates/cli/src/bin/trail.rs index 7d616dd1..21b6dffd 100644 --- a/crates/cli/src/bin/trail.rs +++ b/crates/cli/src/bin/trail.rs @@ -73,13 +73,14 @@ async fn async_main() -> Result<(), BoxError> { address: cmd.address, admin_address: cmd.admin_address, public_dir: cmd.public_dir.map(|p| p.into()), + runtime_root_fs: cmd.runtime_root_fs.map(|p| p.into()), geoip_db_path: cmd.geoip_db_path.map(|p| p.into()), log_responses: cmd.dev || cmd.stderr_logging, dev: cmd.dev, demo: cmd.demo, disable_auth_ui: cmd.disable_auth_ui, cors_allowed_origins: cmd.cors_allowed_origins, - js_runtime_threads: cmd.js_runtime_threads, + runtime_threads: cmd.runtime_threads, tls_key: None, tls_cert: None, }) diff --git a/crates/client/tests/integration_test.rs b/crates/client/tests/integration_test.rs index ecf3af04..349875ee 100644 --- a/crates/client/tests/integration_test.rs +++ b/crates/client/tests/integration_test.rs @@ -35,7 +35,7 @@ fn start_server() -> Result { format!("--data-dir={depot_path}"), "run".to_string(), format!("--address=127.0.0.1:{PORT}"), - "--js-runtime-threads=2".to_string(), + "--runtime-threads=2".to_string(), ]; let child = std::process::Command::new("cargo") .args(&args) diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 9a853d72..257a0d3d 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -7,7 +7,7 @@ rust-version = "1.86" description = "Package to use TrailBase as a framework" homepage = "https://trailbase.io" repository = "https://github.com/trailbaseio/trailbase" -readme = "../README.md" +readme = "../../README.md" exclude = [ "benches/", "tests/", @@ -89,7 +89,9 @@ trailbase-qs = { workspace = true } trailbase-refinery = { workspace = true } trailbase-schema = { workspace = true } trailbase-sqlite = { workspace = true } -ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl"] } +trailbase-wasm-runtime-host = { workspace = true } +trailbase-wasm-common = { workspace = true } +ts-rs = { workspace = true } url = { version = "^2.4.1", default-features = false } utoipa = { version = "5.0.0-beta.0", features = ["axum_extras"] } uuid = { workspace = true } diff --git a/crates/core/src/app_state.rs b/crates/core/src/app_state.rs index d82d9253..fdccdf34 100644 --- a/crates/core/src/app_state.rs +++ b/crates/core/src/app_state.rs @@ -4,6 +4,7 @@ use reactivate::{Merge, Reactive}; use std::path::PathBuf; use std::sync::Arc; use trailbase_schema::QualifiedName; +use trailbase_wasm_runtime_host::Runtime; use crate::auth::jwt::JwtHelper; use crate::auth::options::AuthOptions; @@ -43,6 +44,8 @@ struct InternalState { runtime: RuntimeHandle, + wasm_runtimes: Vec>, + #[cfg(test)] #[allow(unused)] cleanup: Vec>, @@ -52,6 +55,7 @@ pub(crate) struct AppStateArgs { pub data_dir: DataDir, pub public_url: Option, pub public_dir: Option, + pub runtime_root_fs: Option, pub dev: bool, pub demo: bool, pub schema_metadata: SchemaMetadataCache, @@ -60,7 +64,7 @@ pub(crate) struct AppStateArgs { pub logs_conn: trailbase_sqlite::Connection, pub jwt: JwtHelper, pub object_store: Box, - pub js_runtime_threads: Option, + pub runtime_threads: Option, } #[derive(Clone)] @@ -124,7 +128,14 @@ impl AppState { object_store.clone(), ); - let runtime = build_js_runtime(args.conn.clone(), args.js_runtime_threads); + let runtime = build_js_runtime(args.conn.clone(), args.runtime_threads); + let wasm_runtimes = crate::wasm::build_wasm_runtimes_for_components( + args.runtime_threads, + args.conn.clone(), + args.data_dir.root().join("wasm"), + args.runtime_root_fs, + ) + .expect("startup"); AppState { state: Arc::new(InternalState { @@ -163,6 +174,7 @@ impl AppState { schema_metadata, object_store, runtime, + wasm_runtimes, #[cfg(test)] cleanup: vec![], }), @@ -311,6 +323,10 @@ impl AppState { pub(crate) fn script_runtime(&self) -> RuntimeHandle { return self.state.runtime.clone(); } + + pub(crate) fn wasm_runtimes(&self) -> &[Arc] { + return &self.state.wasm_runtimes; + } } #[cfg(test)] @@ -458,6 +474,7 @@ pub async fn test_state(options: Option) -> anyhow::Result Result>, AnyError> { let runtime_handle = state.script_runtime(); if runtime_handle.num_threads() == 0 { @@ -331,7 +333,6 @@ pub(crate) async fn load_routes_and_jobs_from_js_modules( return Ok(None); } - let scripts_dir = state.data_dir().root().join("scripts"); let modules = match Module::load_dir(scripts_dir.clone()) { Ok(modules) => modules, Err(err) => { diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index ed5c9a28..11af2d25 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -22,6 +22,7 @@ mod scheduler; mod schema_metadata; mod server; mod transaction; +mod wasm; #[cfg(test)] mod test; diff --git a/crates/core/src/records/test_utils.rs b/crates/core/src/records/test_utils.rs index 4401157f..557ac562 100644 --- a/crates/core/src/records/test_utils.rs +++ b/crates/core/src/records/test_utils.rs @@ -61,8 +61,9 @@ mod tests { pub fn to_message(v: serde_json::Value) -> Message { return match v { serde_json::Value::Object(ref obj) => { - let keys: Vec<&str> = obj.keys().map(|s| s.as_str()).collect(); - assert_eq!(keys, vec!["mid", "room", "data", "table"], "Got: {keys:?}"); + let mut keys: Vec<&str> = obj.keys().map(|s| s.as_str()).collect(); + keys.sort(); + assert_eq!(keys, ["data", "mid", "room", "table"], "Got: {keys:?}"); serde_json::from_value::(v).unwrap() } _ => panic!("expected object, got {v:?}"), diff --git a/crates/core/src/server/init.rs b/crates/core/src/server/init.rs index 83c014f1..2e292f92 100644 --- a/crates/core/src/server/init.rs +++ b/crates/core/src/server/init.rs @@ -43,12 +43,13 @@ pub struct InitArgs { pub data_dir: DataDir, pub public_url: Option, pub public_dir: Option, + pub runtime_root_fs: Option, pub geoip_db_path: Option, pub address: String, pub dev: bool, pub demo: bool, - pub js_runtime_threads: Option, + pub runtime_threads: Option, } pub async fn init_app_state(args: InitArgs) -> Result<(bool, AppState), InitError> { @@ -138,6 +139,7 @@ pub async fn init_app_state(args: InitArgs) -> Result<(bool, AppState), InitErro data_dir: args.data_dir.clone(), public_url: args.public_url, public_dir: args.public_dir, + runtime_root_fs: args.runtime_root_fs, dev: args.dev, demo: args.demo, schema_metadata, @@ -146,7 +148,7 @@ pub async fn init_app_state(args: InitArgs) -> Result<(bool, AppState), InitErro logs_conn, jwt, object_store, - js_runtime_threads: args.js_runtime_threads, + runtime_threads: args.runtime_threads, }); if new_db { diff --git a/crates/core/src/server/mod.rs b/crates/core/src/server/mod.rs index 853877f3..5baee7b9 100644 --- a/crates/core/src/server/mod.rs +++ b/crates/core/src/server/mod.rs @@ -54,6 +54,9 @@ pub struct ServerOptions { /// Optional path to static assets that will be served at the HTTP root. pub public_dir: Option, + /// Optional path to sandboxed FS root for WASM runtime. + pub runtime_root_fs: Option, + /// Optional path to MaxmindDB geoip database. Can be used to map logged IPs to a geo location. pub geoip_db_path: Option, @@ -75,7 +78,7 @@ pub struct ServerOptions { pub cors_allowed_origins: Vec, /// Number of V8 worker threads. If set to None, default of num available cores will be used. - pub js_runtime_threads: Option, + pub runtime_threads: Option, /// TLS certificate path. pub tls_cert: Option>, @@ -120,15 +123,20 @@ impl Server { date = version_info.commit_date.unwrap_or_default(), ); + validate_path(opts.public_dir.as_ref())?; + validate_path(opts.runtime_root_fs.as_ref())?; + validate_path(opts.geoip_db_path.as_ref())?; + let (new_data_dir, state) = init::init_app_state(InitArgs { data_dir: opts.data_dir.clone(), public_url: opts.public_url.clone(), public_dir: opts.public_dir.clone(), + runtime_root_fs: opts.runtime_root_fs.clone(), geoip_db_path: opts.geoip_db_path.clone(), address: opts.address.clone(), dev: opts.dev, demo: opts.demo, - js_runtime_threads: opts.js_runtime_threads, + runtime_threads: opts.runtime_threads, }) .await?; @@ -170,18 +178,31 @@ impl Server { .map_err(|err| InitError::CustomInit(err.to_string()))?; } - #[cfg(feature = "v8")] - let js_routes: Option> = - crate::js::runtime::load_routes_and_jobs_from_js_modules(&state) - .await - .map_err(|err| InitError::ScriptError(err.to_string()))?; + let mut custom_routers: Vec> = vec![]; - #[cfg(not(feature = "v8"))] - let js_routes: Option> = None; + #[cfg(feature = "v8")] + if let Some(js_router) = crate::js::runtime::load_routes_and_jobs_from_js_modules( + &state, + state.data_dir().root().join("scripts"), + ) + .await + .map_err(|err| InitError::ScriptError(err.to_string()))? + { + custom_routers.push(js_router); + } + + for rt in state.wasm_runtimes() { + if let Some(wasm_router) = crate::wasm::install_routes_and_jobs(&state, rt.clone()) + .await + .map_err(|err| InitError::ScriptError(err.to_string()))? + { + custom_routers.push(wasm_router); + } + } Ok(Self { state: state.clone(), - main_router: Self::build_main_router(&state, &opts, js_routes).await, + main_router: Self::build_main_router(&state, &opts, custom_routers).await, admin_router: Self::build_independent_admin_router(&state, &opts), tls: Self::load_tls(&opts), }) @@ -200,6 +221,7 @@ impl Server { stream.recv().await; // TODO: Re-load JS/TS. + // TODO: Re-load WASM. info!("Received SIGHUP: re-apply migations then re-load config."); // Re-apply migrations. This needs to happen before reloading the config, which is @@ -333,7 +355,7 @@ impl Server { async fn build_main_router( state: &AppState, opts: &ServerOptions, - custom_router: Option>, + custom_routers: Vec>, ) -> (String, Router<()>) { let enable_transactions = state.access_config(|conn| conn.server.enable_record_transactions.unwrap_or(false)); @@ -352,7 +374,7 @@ impl Server { router = router.merge(auth::auth_ui_router()); } - if let Some(custom_router) = custom_router { + for custom_router in custom_routers { router = router.merge(custom_router); } @@ -579,6 +601,11 @@ async fn start_listen( } }; + #[cfg(not(feature = "v8"))] + tokio_rustls::rustls::crypto::ring::default_provider() + .install_default() + .expect("Failed to install TLS provider"); + let server_config = ServerConfig::builder() .with_no_client_auth() .with_single_cert(vec![cert], key) @@ -616,3 +643,12 @@ async fn start_listen( } }; } + +fn validate_path(path: Option<&PathBuf>) -> Result<(), InitError> { + if let Some(path) = path { + if !std::fs::exists(path)? { + return Err(InitError::CustomInit(format!("Path not found: {path:?}"))); + }; + } + return Ok(()); +} diff --git a/crates/core/src/wasm/mod.rs b/crates/core/src/wasm/mod.rs new file mode 100644 index 00000000..c26f1e25 --- /dev/null +++ b/crates/core/src/wasm/mod.rs @@ -0,0 +1,230 @@ +use axum::Router; +use axum::extract::{RawPathParams, Request}; +use bytes::Bytes; +use http_body_util::{BodyExt, combinators::BoxBody}; +use hyper::StatusCode; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::Arc; +use trailbase_wasm_common::{HttpContext, HttpContextKind, HttpContextUser}; +use trailbase_wasm_runtime_host::exports::trailbase::runtime::init_endpoint::MethodType; +use trailbase_wasm_runtime_host::{Error as WasmError, KvStore, Runtime}; + +use crate::AppState; +use crate::User; + +type AnyError = Box; + +pub(crate) fn build_wasm_runtimes_for_components( + n_threads: Option, + conn: trailbase_sqlite::Connection, + components_path: PathBuf, + fs_root_path: Option, +) -> Result>, AnyError> { + let shared_kv_store = KvStore::new(); + let mut runtimes: Vec> = vec![]; + + if let Ok(entries) = std::fs::read_dir(components_path) { + for entry in entries { + let Ok(entry) = entry else { + continue; + }; + + let Ok(metadata) = entry.metadata() else { + continue; + }; + + if !metadata.is_file() { + continue; + } + let path = entry.path(); + let Some(extension) = path.extension().and_then(|e| e.to_str()) else { + continue; + }; + + if extension == "wasm" { + let n_threads = n_threads + .or(std::thread::available_parallelism().ok().map(|n| n.get())) + .unwrap_or(1); + + runtimes.push(Arc::new(Runtime::new( + n_threads, + path, + conn.clone(), + shared_kv_store.clone(), + fs_root_path.clone(), + )?)); + } + } + } + + if runtimes.is_empty() { + log::debug!("No WASM component found"); + } + + return Ok(runtimes); +} + +pub(crate) async fn install_routes_and_jobs( + state: &AppState, + runtime: Arc, +) -> Result>, AnyError> { + let init_result = runtime + .call(async |instance| { + return instance.call_init().await; + }) + .await??; + + for (name, spec) in &init_result.job_handlers { + let schedule = cron::Schedule::from_str(spec)?; + let runtime = runtime.clone(); + let name_clone = name.to_string(); + + let Some(job) = state.jobs().new_job( + None, + name, + schedule, + crate::scheduler::build_callback(move || { + let name = name_clone.clone(); + let runtime = runtime.clone(); + + return async move { + runtime + .call(async move |instance| -> Result<(), WasmError> { + let request = hyper::Request::builder() + // NOTE: We cannot use a custom-scheme, since the wasi http + // implementation rejects everything but http and https. + .uri(format!("http://__job/?name={name}")) + .header( + "__context", + to_header_value(&HttpContext { + kind: HttpContextKind::Job, + registered_path: name.clone(), + path_params: vec![], + user: None, + })?, + ) + .body(empty()) + .unwrap_or_default(); + + instance.call_incoming_http_handler(request).await?; + + return Ok(()); + }) + .await??; + + Ok::<_, AnyError>(()) + }; + }), + ) else { + return Err("Failed to add job".into()); + }; + + job.start(); + } + + log::debug!("Got {} WASM routes", init_result.http_handlers.len()); + + let mut router = Router::::new(); + for (method, path) in &init_result.http_handlers { + let runtime = runtime.clone(); + + log::debug!("Installing WASM route: {method:?}: {path}"); + + let handler = { + let path = path.clone(); + + move |params: RawPathParams, user: Option, req: Request| async move { + log::debug!( + "Host received WASM HTTP request: {params:?}, {user:?}, {}", + req.uri() + ); + + let result = runtime + .call( + async move |instance| -> Result { + let (mut parts, body) = req.into_parts(); + let bytes = body + .collect() + .await + .map_err(|_err| WasmError::ChannelClosed)? + .to_bytes(); + + parts.headers.insert( + "__context", + to_header_value(&HttpContext { + kind: HttpContextKind::Http, + registered_path: path.clone(), + path_params: params + .iter() + .map(|(name, value)| (name.to_string(), value.to_string())) + .collect(), + user: user.map(|u| HttpContextUser { + id: u.id, + email: u.email, + csrf_token: u.csrf_token, + }), + })?, + ); + + let request = hyper::Request::from_parts( + parts, + BoxBody::new(http_body_util::Full::new(bytes).map_err(|_| unreachable!())), + ); + + let response = instance.call_incoming_http_handler(request).await?; + + let (parts, body) = response.into_parts(); + let bytes = body + .collect() + .await + .map_err(|_err| WasmError::ChannelClosed)? + .to_bytes(); + + return Ok(axum::response::Response::from_parts(parts, bytes.into())); + }, + ) + .await; + + return match result { + Ok(Ok(r)) => r, + Ok(Err(err)) => internal_error_response(err), + Err(err) => internal_error_response(err), + }; + } + }; + + router = router.route( + path, + match method { + MethodType::Delete => axum::routing::delete(handler), + MethodType::Get => axum::routing::get(handler), + MethodType::Head => axum::routing::head(handler), + MethodType::Options => axum::routing::options(handler), + MethodType::Patch => axum::routing::patch(handler), + MethodType::Post => axum::routing::post(handler), + MethodType::Put => axum::routing::put(handler), + MethodType::Trace => axum::routing::trace(handler), + MethodType::Connect => axum::routing::connect(handler), + }, + ); + } + + return Ok(Some(router)); +} + +fn empty() -> BoxBody { + return BoxBody::new(http_body_util::Empty::new().map_err(|_| unreachable!())); +} + +fn to_header_value(context: &HttpContext) -> Result { + return hyper::http::HeaderValue::from_bytes(&serde_json::to_vec(&context).unwrap_or_default()) + .map_err(|_err| WasmError::Encoding); +} + +fn internal_error_response(err: impl std::string::ToString) -> axum::response::Response { + return axum::response::Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(err.to_string().into()) + .unwrap_or_default(); +} diff --git a/crates/core/tests/admin_permissions_test.rs b/crates/core/tests/admin_permissions_test.rs index 5b38f972..2544ae8d 100644 --- a/crates/core/tests/admin_permissions_test.rs +++ b/crates/core/tests/admin_permissions_test.rs @@ -26,7 +26,7 @@ fn test_admin_permissions() { dev: false, disable_auth_ui: false, cors_allowed_origins: vec![], - js_runtime_threads: None, + runtime_threads: None, ..Default::default() }) .await diff --git a/crates/core/tests/integration_test.rs b/crates/core/tests/integration_test.rs index c2fe07da..cc57a9be 100644 --- a/crates/core/tests/integration_test.rs +++ b/crates/core/tests/integration_test.rs @@ -48,7 +48,7 @@ async fn test_record_apis() { dev: false, disable_auth_ui: false, cors_allowed_origins: vec![], - js_runtime_threads: None, + runtime_threads: None, ..Default::default() }) .await diff --git a/crates/core/tests/tls_test.rs b/crates/core/tests/tls_test.rs index 8c83439d..813cb911 100644 --- a/crates/core/tests/tls_test.rs +++ b/crates/core/tests/tls_test.rs @@ -32,7 +32,7 @@ fn test_https_serving() { dev: false, disable_auth_ui: false, cors_allowed_origins: vec![], - js_runtime_threads: None, + runtime_threads: None, tls_key: Some(tls_key), tls_cert: Some(cert.der().clone()), ..Default::default() diff --git a/crates/schema/Cargo.toml b/crates/schema/Cargo.toml index a79403d8..b926640b 100644 --- a/crates/schema/Cargo.toml +++ b/crates/schema/Cargo.toml @@ -26,7 +26,7 @@ sqlite3-parser = "0.15.0" thiserror = "2.0.12" tokio = { workspace = true } trailbase-extension = { workspace = true } -ts-rs = { version = "11", features = ["uuid-impl", "serde-json-impl"] } +ts-rs = { workspace = true } uuid = { workspace = true } [dev-dependencies] diff --git a/crates/wasi-keyvalue/Cargo.toml b/crates/wasi-keyvalue/Cargo.toml new file mode 100644 index 00000000..71a067fb --- /dev/null +++ b/crates/wasi-keyvalue/Cargo.toml @@ -0,0 +1,17 @@ +# Fork of wasmtime/crates/wasi-keyvalue. +[package] +name = "trailbase-wasi-keyvalue" +version = "0.1.0" +edition = "2024" +license = "OSL-3.0" +description = "Sync WASI KV store - fork of upstream" +homepage = "https://trailbase.io" + +[dependencies] +anyhow = "1.0.99" +parking_lot = { workspace = true } +wasmtime = { workspace = true } + +[dev-dependencies] +wasmtime-wasi = { workspace = true } +tokio = { workspace = true } diff --git a/crates/wasi-keyvalue/src/lib.rs b/crates/wasi-keyvalue/src/lib.rs new file mode 100644 index 00000000..12fd61dd --- /dev/null +++ b/crates/wasi-keyvalue/src/lib.rs @@ -0,0 +1,233 @@ +//! # Wasmtime's [wasi-keyvalue] Implementation +//! +//! This crate provides a Wasmtime host implementation of the [wasi-keyvalue] +//! API. With this crate, the runtime can run components that call APIs in +//! [wasi-keyvalue] and provide components with access to key-value storages. +//! +//! Currently supported storage backends: +//! * In-Memory (empty identifier) + +#![allow(clippy::needless_return)] +#![deny(missing_docs)] +#![forbid(clippy::unwrap_used)] +#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)] + +mod generated { + wasmtime::component::bindgen!({ + path: "wit", + world: "wasi:keyvalue/imports", + imports: { default: trappable }, + with: { + "wasi:keyvalue/store/bucket": crate::Bucket, + }, + trappable_error_type: { + "wasi:keyvalue/store/error" => crate::Error, + }, + }); +} + +use self::generated::wasi::keyvalue; + +use anyhow::Result; +use parking_lot::RwLock; +use std::collections::HashMap; +use std::sync::Arc; +use wasmtime::component::{HasData, Resource, ResourceTable, ResourceTableError}; + +#[doc(hidden)] +pub enum Error { + NoSuchStore, + AccessDenied, + Other(String), +} + +impl From for Error { + fn from(err: ResourceTableError) -> Self { + Self::Other(err.to_string()) + } +} + +type InternalStore = Arc>>>; + +/// The practical type for the inmemory Store. +#[derive(Clone, Default)] +pub struct Store { + store: Arc>>>, +} + +impl Store { + /// New shared storage for WASI KV implementation. + pub fn new() -> Self { + return Store { + store: Arc::new(RwLock::new(HashMap::new())), + }; + } +} + +#[doc(hidden)] +pub struct Bucket { + in_memory_data: InternalStore, +} + +/// Capture the state necessary for use in the `wasi-keyvalue` API implementation. +pub struct WasiKeyValueCtx { + in_memory_data: InternalStore, +} + +impl WasiKeyValueCtx { + /// Inject shared data. + pub fn new(data: Store) -> Self { + return Self { + in_memory_data: data.store, + }; + } +} + +/// A wrapper capturing the needed internal `wasi-keyvalue` state. +pub struct WasiKeyValue<'a> { + ctx: &'a WasiKeyValueCtx, + table: &'a mut ResourceTable, +} + +impl<'a> WasiKeyValue<'a> { + /// Create a new view into the `wasi-keyvalue` state. + pub fn new(ctx: &'a WasiKeyValueCtx, table: &'a mut ResourceTable) -> Self { + Self { ctx, table } + } +} + +impl keyvalue::store::Host for WasiKeyValue<'_> { + fn open(&mut self, identifier: String) -> Result, Error> { + match identifier.as_str() { + "" => Ok(self.table.push(Bucket { + in_memory_data: self.ctx.in_memory_data.clone(), + })?), + _ => Err(Error::NoSuchStore), + } + } + + fn convert_error(&mut self, err: Error) -> Result { + match err { + Error::NoSuchStore => Ok(keyvalue::store::Error::NoSuchStore), + Error::AccessDenied => Ok(keyvalue::store::Error::AccessDenied), + Error::Other(e) => Ok(keyvalue::store::Error::Other(e)), + } + } +} + +impl keyvalue::store::HostBucket for WasiKeyValue<'_> { + fn get(&mut self, bucket: Resource, key: String) -> Result>, Error> { + let bucket = self.table.get_mut(&bucket)?; + Ok(bucket.in_memory_data.read().get(&key).cloned()) + } + + fn set(&mut self, bucket: Resource, key: String, value: Vec) -> Result<(), Error> { + let bucket = self.table.get_mut(&bucket)?; + bucket.in_memory_data.write().insert(key, value); + Ok(()) + } + + fn delete(&mut self, bucket: Resource, key: String) -> Result<(), Error> { + let bucket = self.table.get_mut(&bucket)?; + bucket.in_memory_data.write().remove(&key); + Ok(()) + } + + fn exists(&mut self, bucket: Resource, key: String) -> Result { + let bucket = self.table.get_mut(&bucket)?; + Ok(bucket.in_memory_data.read().contains_key(&key)) + } + + fn list_keys( + &mut self, + bucket: Resource, + cursor: Option, + ) -> Result { + let bucket = self.table.get_mut(&bucket)?; + let keys: Vec = bucket.in_memory_data.read().keys().cloned().collect(); + let cursor = cursor.unwrap_or(0) as usize; + let keys_slice = &keys[cursor..]; + Ok(keyvalue::store::KeyResponse { + keys: keys_slice.to_vec(), + cursor: None, + }) + } + + fn drop(&mut self, bucket: Resource) -> Result<()> { + self.table.delete(bucket)?; + Ok(()) + } +} + +impl keyvalue::atomics::Host for WasiKeyValue<'_> { + fn increment(&mut self, bucket: Resource, key: String, delta: u64) -> Result { + let bucket = self.table.get_mut(&bucket)?; + let mut data = bucket.in_memory_data.write(); + let value = data.entry(key.clone()).or_insert(b"0".to_vec()); + + let current_value = String::from_utf8(value.clone()) + .map_err(|e| Error::Other(e.to_string()))? + .parse::() + .map_err(|e| Error::Other(e.to_string()))?; + let new_value = current_value + delta; + + *value = new_value.to_string().into_bytes(); + Ok(new_value) + } +} + +impl keyvalue::batch::Host for WasiKeyValue<'_> { + fn get_many( + &mut self, + bucket: Resource, + keys: Vec, + ) -> Result)>>, Error> { + let bucket = self.table.get_mut(&bucket)?; + let lock = bucket.in_memory_data.read(); + Ok( + keys + .into_iter() + .map(|key| lock.get(&key).map(|value| (key.clone(), value.clone()))) + .collect(), + ) + } + + fn set_many( + &mut self, + bucket: Resource, + key_values: Vec<(String, Vec)>, + ) -> Result<(), Error> { + let bucket = self.table.get_mut(&bucket)?; + let mut lock = bucket.in_memory_data.write(); + for (key, value) in key_values { + lock.insert(key, value); + } + Ok(()) + } + + fn delete_many(&mut self, bucket: Resource, keys: Vec) -> Result<(), Error> { + let bucket = self.table.get_mut(&bucket)?; + let mut lock = bucket.in_memory_data.write(); + for key in keys { + lock.remove(&key); + } + Ok(()) + } +} + +/// Add all the `wasi-keyvalue` world's interfaces to a [`wasmtime::component::Linker`]. +pub fn add_to_linker( + l: &mut wasmtime::component::Linker, + f: fn(&mut T) -> WasiKeyValue<'_>, +) -> Result<()> { + keyvalue::store::add_to_linker::<_, HasWasiKeyValue>(l, f)?; + keyvalue::atomics::add_to_linker::<_, HasWasiKeyValue>(l, f)?; + keyvalue::batch::add_to_linker::<_, HasWasiKeyValue>(l, f)?; + Ok(()) +} + +struct HasWasiKeyValue; + +impl HasData for HasWasiKeyValue { + type Data<'a> = WasiKeyValue<'a>; +} diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/atomic.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/atomic.wit new file mode 100644 index 00000000..1e8106ec --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue/atomic.wit @@ -0,0 +1,22 @@ +/// A keyvalue interface that provides atomic operations. +/// +/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to +/// fail, it will appear to the invoker of the atomic operation that the action either completed +/// successfully or did nothing at all. +/// +/// Please note that this interface is bare functions that take a reference to a bucket. This is to +/// get around the current lack of a way to "extend" a resource with additional methods inside of +/// wit. Future version of the interface will instead extend these methods on the base `bucket` +/// resource. +interface atomics { + use store.{bucket, error}; + + /// Atomically increment the value associated with the key in the store by the given delta. It + /// returns the new value. + /// + /// If the key does not exist in the store, it creates a new key-value pair with the value set + /// to the given delta. + /// + /// If any other error occurs, it returns an `Err(error)`. + increment: func(bucket: borrow, key: string, delta: u64) -> result; +} diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/batch.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/batch.wit new file mode 100644 index 00000000..c642dd3f --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue/batch.wit @@ -0,0 +1,63 @@ +/// A keyvalue interface that provides batch operations. +/// +/// A batch operation is an operation that operates on multiple keys at once. +/// +/// Batch operations are useful for reducing network round-trip time. For example, if you want to +/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1 +/// batch get operation. The batch operation is faster because it only needs to make 1 network call +/// instead of 100. +/// +/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some +/// of the keys may have been modified and some may not. +/// +/// This interface does has the same consistency guarantees as the `store` interface, meaning that +/// you should be able to "read your writes." +/// +/// Please note that this interface is bare functions that take a reference to a bucket. This is to +/// get around the current lack of a way to "extend" a resource with additional methods inside of +/// wit. Future version of the interface will instead extend these methods on the base `bucket` +/// resource. +interface batch { + use store.{bucket, error}; + + /// Get the key-value pairs associated with the keys in the store. It returns a list of + /// key-value pairs. + /// + /// If any of the keys do not exist in the store, it returns a `none` value for that pair in the + /// list. + /// + /// MAY show an out-of-date value if there are concurrent writes to the store. + /// + /// If any other error occurs, it returns an `Err(error)`. + get-many: func(bucket: borrow, keys: list) -> result>>>, error>; + + /// Set the values associated with the keys in the store. If the key already exists in the + /// store, it overwrites the value. + /// + /// Note that the key-value pairs are not guaranteed to be set in the order they are provided. + /// + /// If any of the keys do not exist in the store, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not + /// rollback the key-value pairs that were already set. Thus, this batch operation does not + /// guarantee atomicity, implying that some key-value pairs could be set while others might + /// fail. + /// + /// Other concurrent operations may also be able to see the partial results. + set-many: func(bucket: borrow, key-values: list>>) -> result<_, error>; + + /// Delete the key-value pairs associated with the keys in the store. + /// + /// Note that the key-value pairs are not guaranteed to be deleted in the order they are + /// provided. + /// + /// If any of the keys do not exist in the store, it skips the key. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not + /// rollback the key-value pairs that were already deleted. Thus, this batch operation does not + /// guarantee atomicity, implying that some key-value pairs could be deleted while others might + /// fail. + /// + /// Other concurrent operations may also be able to see the partial results. + delete-many: func(bucket: borrow, keys: list) -> result<_, error>; +} diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/store.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/store.wit new file mode 100644 index 00000000..91273536 --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue/store.wit @@ -0,0 +1,122 @@ +/// A keyvalue interface that provides eventually consistent key-value operations. +/// +/// Each of these operations acts on a single key-value pair. +/// +/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is +/// the common denominator for all data types defined by different key-value stores to handle data, +/// ensuring compatibility between different key-value stores. Note: the clients will be expecting +/// serialization/deserialization overhead to be handled by the key-value store. The value could be +/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects. +/// +/// Data consistency in a key value store refers to the guarantee that once a write operation +/// completes, all subsequent read operations will return the value that was written. +/// +/// Any implementation of this interface must have enough consistency to guarantee "reading your +/// writes." In particular, this means that the client should never get a value that is older than +/// the one it wrote, but it MAY get a newer value if one was written around the same time. These +/// guarantees only apply to the same client (which will likely be provided by the host or an +/// external capability of some kind). In this context a "client" is referring to the caller or +/// guest that is consuming this interface. Once a write request is committed by a specific client, +/// all subsequent read requests by the same client will reflect that write or any subsequent +/// writes. Another client running in a different context may or may not immediately see the result +/// due to the replication lag. As an example of all of this, if a value at a given key is A, and +/// the client writes B, then immediately reads, it should get B. If something else writes C in +/// quick succession, then the client may get C. However, a client running in a separate context may +/// still see A or B +interface store { + /// The set of errors which may be raised by functions in this package + variant error { + /// The host does not recognize the store identifier requested. + no-such-store, + + /// The requesting component does not have access to the specified store + /// (which may or may not exist). + access-denied, + + /// Some implementation-specific error has occurred (e.g. I/O) + other(string) + } + + /// A response to a `list-keys` operation. + record key-response { + /// The list of keys returned by the query. + keys: list, + /// The continuation token to use to fetch the next page of keys. If this is `null`, then + /// there are no more keys to fetch. + cursor: option + } + + /// Get the bucket with the specified identifier. + /// + /// `identifier` must refer to a bucket provided by the host. + /// + /// `error::no-such-store` will be raised if the `identifier` is not recognized. + open: func(identifier: string) -> result; + + /// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the + /// bucket, and the bucket itself acts as a collection of all these entries. + /// + /// It is worth noting that the exact terminology for bucket in key-value stores can very + /// depending on the specific implementation. For example: + /// + /// 1. Amazon DynamoDB calls a collection of key-value pairs a table + /// 2. Redis has hashes, sets, and sorted sets as different types of collections + /// 3. Cassandra calls a collection of key-value pairs a column family + /// 4. MongoDB calls a collection of key-value pairs a collection + /// 5. Riak calls a collection of key-value pairs a bucket + /// 6. Memcached calls a collection of key-value pairs a slab + /// 7. Azure Cosmos DB calls a collection of key-value pairs a container + /// + /// In this interface, we use the term `bucket` to refer to a collection of key-value pairs + resource bucket { + /// Get the value associated with the specified `key` + /// + /// The value is returned as an option. If the key-value pair exists in the + /// store, it returns `Ok(value)`. If the key does not exist in the + /// store, it returns `Ok(none)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + get: func(key: string) -> result>, error>; + + /// Set the value associated with the key in the store. If the key already + /// exists in the store, it overwrites the value. + /// + /// If the key does not exist in the store, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. + set: func(key: string, value: list) -> result<_, error>; + + /// Delete the key-value pair associated with the key in the store. + /// + /// If the key does not exist in the store, it does nothing. + /// + /// If any other error occurs, it returns an `Err(error)`. + delete: func(key: string) -> result<_, error>; + + /// Check if the key exists in the store. + /// + /// If the key exists in the store, it returns `Ok(true)`. If the key does + /// not exist in the store, it returns `Ok(false)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + exists: func(key: string) -> result; + + /// Get all the keys in the store with an optional cursor (for use in pagination). It + /// returns a list of keys. Please note that for most KeyValue implementations, this is a + /// can be a very expensive operation and so it should be used judiciously. Implementations + /// can return any number of keys in a single response, but they should never attempt to + /// send more data than is reasonable (i.e. on a small edge device, this may only be a few + /// KB, while on a large machine this could be several MB). Any response should also return + /// a cursor that can be used to fetch the next page of keys. See the `key-response` record + /// for more information. + /// + /// Note that the keys are not guaranteed to be returned in any particular order. + /// + /// If the store is empty, it returns an empty list. + /// + /// MAY show an out-of-date list of keys if there are concurrent writes to the store. + /// + /// If any error occurs, it returns an `Err(error)`. + list-keys: func(cursor: option) -> result; + } +} diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/watch.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/watch.wit new file mode 100644 index 00000000..4c77960c --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue/watch.wit @@ -0,0 +1,16 @@ +/// A keyvalue interface that provides watch operations. +/// +/// This interface is used to provide event-driven mechanisms to handle +/// keyvalue changes. +interface watcher { + /// A keyvalue interface that provides handle-watch operations. + use store.{bucket}; + + /// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket` + /// that can be used to interact with the store. + on-set: func(bucket: bucket, key: string, value: list); + + /// Handle the `delete` event for the given bucket and key. It includes a reference to the + /// `bucket` that can be used to interact with the store. + on-delete: func(bucket: bucket, key: string); +} diff --git a/crates/wasi-keyvalue/wit/deps/keyvalue/world.wit b/crates/wasi-keyvalue/wit/deps/keyvalue/world.wit new file mode 100644 index 00000000..8dd33046 --- /dev/null +++ b/crates/wasi-keyvalue/wit/deps/keyvalue/world.wit @@ -0,0 +1,26 @@ +package wasi:keyvalue@0.2.0-draft; + +/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores. +/// Components targeting this world will be able to do: +/// +/// 1. CRUD (create, read, update, delete) operations on key-value stores. +/// 2. Atomic `increment` and CAS (compare-and-swap) operations. +/// 3. Batch operations that can reduce the number of round trips to the network. +world imports { + /// The `store` capability allows the component to perform eventually consistent operations on + /// the key-value store. + import store; + + /// The `atomic` capability allows the component to perform atomic / `increment` and CAS + /// (compare-and-swap) operations. + import atomics; + + /// The `batch` capability allows the component to perform eventually consistent batch + /// operations that can reduce the number of round trips to the network. + import batch; +} + +world watch-service { + include imports; + export watcher; +} diff --git a/crates/wasi-keyvalue/wit/world.wit b/crates/wasi-keyvalue/wit/world.wit new file mode 100644 index 00000000..af5b0bd5 --- /dev/null +++ b/crates/wasi-keyvalue/wit/world.wit @@ -0,0 +1,6 @@ +// We actually don't use this; it's just to let bindgen! find the corresponding world in wit/deps. +package wasmtime:wasi-keyvalue; + +world bindings { + include wasi:keyvalue/imports@0.2.0-draft; +} diff --git a/crates/wasm-runtime-common/Cargo.toml b/crates/wasm-runtime-common/Cargo.toml new file mode 100644 index 00000000..167243f4 --- /dev/null +++ b/crates/wasm-runtime-common/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "trailbase-wasm-common" +version = "0.1.0" +edition = "2024" +license = "OSL-3.0" +description = "WASM runtime for the TrailBase framework" +homepage = "https://trailbase.io" +exclude = [ + "**/node_modules/", + "**/dist/", +] + +[dependencies] +serde = { version = "^1.0.203", features = ["derive"] } +serde_json = "^1.0.117" +ts-rs = { workspace = true } diff --git a/crates/wasm-runtime-common/src/lib.rs b/crates/wasm-runtime-common/src/lib.rs new file mode 100644 index 00000000..eaf206a8 --- /dev/null +++ b/crates/wasm-runtime-common/src/lib.rs @@ -0,0 +1,51 @@ +#![forbid(unsafe_code, clippy::unwrap_used)] +#![allow(clippy::needless_return)] +#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)] + +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct SqliteRequest { + pub query: String, + pub params: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub enum SqliteResponse { + Query { rows: Vec> }, + Execute { rows_affected: usize }, + Error(String), + TxBegin, + TxCommit, + TxRollback, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +pub enum HttpContextKind { + /// An incoming http request. + Http, + /// An incoming job request. + Job, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +pub struct HttpContextUser { + /// Url-safe Base64 encoded id of the current user. + pub id: String, + /// E-mail of the current user. + pub email: String, + /// The "expected" CSRF token as included in the auth token claims [User] was constructed from. + pub csrf_token: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize, TS)] +#[ts(export)] +pub struct HttpContext { + pub kind: HttpContextKind, + pub registered_path: String, + pub path_params: Vec<(String, String)>, + pub user: Option, +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/command.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/command.wit new file mode 100644 index 00000000..6d3cc83f --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.2.6; + +@since(version = 0.2.0) +world command { + @since(version = 0.2.0) + include imports; + + @since(version = 0.2.0) + export run; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/environment.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/environment.wit new file mode 100644 index 00000000..2f449bd7 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/environment.wit @@ -0,0 +1,22 @@ +@since(version = 0.2.0) +interface environment { + /// Get the POSIX-style environment variables. + /// + /// Each environment variable is provided as a pair of string variable names + /// and string value. + /// + /// Morally, these are a value import, but until value imports are available + /// in the component model, this import function should return the same + /// values each time it is called. + @since(version = 0.2.0) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.2.0) + get-arguments: func() -> list; + + /// Return a path that programs should use as their initial current working + /// directory, interpreting `.` as shorthand for this. + @since(version = 0.2.0) + initial-cwd: func() -> option; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/exit.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/exit.wit new file mode 100644 index 00000000..427935c8 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/exit.wit @@ -0,0 +1,17 @@ +@since(version = 0.2.0) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.2.0) + exit: func(status: result); + + /// Exit the current instance and any linked instances, reporting the + /// specified status code to the host. + /// + /// The meaning of the code depends on the context, with 0 usually meaning + /// "success", and other values indicating various types of failure. + /// + /// This function does not return; the effect is analogous to a trap, but + /// without the connotation that something bad has happened. + @unstable(feature = cli-exit-with-code) + exit-with-code: func(status-code: u8); +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/imports.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/imports.wit new file mode 100644 index 00000000..d9fd0171 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/imports.wit @@ -0,0 +1,36 @@ +package wasi:cli@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + include wasi:clocks/imports@0.2.6; + @since(version = 0.2.0) + include wasi:filesystem/imports@0.2.6; + @since(version = 0.2.0) + include wasi:sockets/imports@0.2.6; + @since(version = 0.2.0) + include wasi:random/imports@0.2.6; + @since(version = 0.2.0) + include wasi:io/imports@0.2.6; + + @since(version = 0.2.0) + import environment; + @since(version = 0.2.0) + import exit; + @since(version = 0.2.0) + import stdin; + @since(version = 0.2.0) + import stdout; + @since(version = 0.2.0) + import stderr; + @since(version = 0.2.0) + import terminal-input; + @since(version = 0.2.0) + import terminal-output; + @since(version = 0.2.0) + import terminal-stdin; + @since(version = 0.2.0) + import terminal-stdout; + @since(version = 0.2.0) + import terminal-stderr; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/run.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/run.wit new file mode 100644 index 00000000..655346ef --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/run.wit @@ -0,0 +1,6 @@ +@since(version = 0.2.0) +interface run { + /// Run the program. + @since(version = 0.2.0) + run: func() -> result; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/stdio.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/stdio.wit new file mode 100644 index 00000000..cb8aea2d --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/stdio.wit @@ -0,0 +1,26 @@ +@since(version = 0.2.0) +interface stdin { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream}; + + @since(version = 0.2.0) + get-stdin: func() -> input-stream; +} + +@since(version = 0.2.0) +interface stdout { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{output-stream}; + + @since(version = 0.2.0) + get-stdout: func() -> output-stream; +} + +@since(version = 0.2.0) +interface stderr { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{output-stream}; + + @since(version = 0.2.0) + get-stderr: func() -> output-stream; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/terminal.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/terminal.wit new file mode 100644 index 00000000..d305498c --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/cli/terminal.wit @@ -0,0 +1,62 @@ +/// Terminal input. +/// +/// In the future, this may include functions for disabling echoing, +/// disabling input buffering so that keyboard events are sent through +/// immediately, querying supported features, and so on. +@since(version = 0.2.0) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.2.0) + resource terminal-input; +} + +/// Terminal output. +/// +/// In the future, this may include functions for querying the terminal +/// size, being notified of terminal size changes, querying supported +/// features, and so on. +@since(version = 0.2.0) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.2.0) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stdin { + @since(version = 0.2.0) + use terminal-input.{terminal-input}; + + /// If stdin is connected to a terminal, return a `terminal-input` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stdout { + @since(version = 0.2.0) + use terminal-output.{terminal-output}; + + /// If stdout is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.2.0) +interface terminal-stderr { + @since(version = 0.2.0) + use terminal-output.{terminal-output}; + + /// If stderr is connected to a terminal, return a `terminal-output` handle + /// allowing further interaction with it. + @since(version = 0.2.0) + get-terminal-stderr: func() -> option; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/monotonic-clock.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/monotonic-clock.wit new file mode 100644 index 00000000..f3bc8391 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/monotonic-clock.wit @@ -0,0 +1,50 @@ +package wasi:clocks@0.2.6; +/// WASI Monotonic Clock is a clock API intended to let users measure elapsed +/// time. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A monotonic clock is a clock which has an unspecified initial value, and +/// successive reads of the clock will produce non-decreasing values. +@since(version = 0.2.0) +interface monotonic-clock { + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + + /// An instant in time, in nanoseconds. An instant is relative to an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.2.0) + type instant = u64; + + /// A duration of time, in nanoseconds. + @since(version = 0.2.0) + type duration = u64; + + /// Read the current value of the clock. + /// + /// The clock is monotonic, therefore calling this function repeatedly will + /// produce a sequence of non-decreasing values. + @since(version = 0.2.0) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.2.0) + resolution: func() -> duration; + + /// Create a `pollable` which will resolve once the specified instant + /// has occurred. + @since(version = 0.2.0) + subscribe-instant: func( + when: instant, + ) -> pollable; + + /// Create a `pollable` that will resolve after the specified duration has + /// elapsed from the time this function is invoked. + @since(version = 0.2.0) + subscribe-duration: func( + when: duration, + ) -> pollable; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/timezone.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/timezone.wit new file mode 100644 index 00000000..ca98ad15 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.2.6; + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use wall-clock.{datetime}; + + /// Return information needed to display the given `datetime`. This includes + /// the UTC offset, the time zone name, and a flag indicating whether + /// daylight saving time is active. + /// + /// If the timezone cannot be determined for the given `datetime`, return a + /// `timezone-display` for `UTC` with a `utc-offset` of 0 and no daylight + /// saving time. + @unstable(feature = clocks-timezone) + display: func(when: datetime) -> timezone-display; + + /// The same as `display`, but only return the UTC offset. + @unstable(feature = clocks-timezone) + utc-offset: func(when: datetime) -> s32; + + /// Information useful for displaying the timezone of a specific `datetime`. + /// + /// This information may vary within a single `timezone` to reflect daylight + /// saving time adjustments. + @unstable(feature = clocks-timezone) + record timezone-display { + /// The number of seconds difference between UTC time and the local + /// time of the timezone. + /// + /// The returned value will always be less than 86400 which is the + /// number of seconds in a day (24*60*60). + /// + /// In implementations that do not expose an actual time zone, this + /// should return 0. + utc-offset: s32, + + /// The abbreviated name of the timezone to display to a user. The name + /// `UTC` indicates Coordinated Universal Time. Otherwise, this should + /// reference local standards for the name of the time zone. + /// + /// In implementations that do not expose an actual time zone, this + /// should be the string `UTC`. + /// + /// In time zones that do not have an applicable name, a formatted + /// representation of the UTC offset may be returned, such as `-04:00`. + name: string, + + /// Whether daylight saving time is active. + /// + /// In implementations that do not expose an actual time zone, this + /// should return false. + in-daylight-saving-time: bool, + } +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/wall-clock.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/wall-clock.wit new file mode 100644 index 00000000..76636a0c --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.2.6; +/// WASI Wall Clock is a clock API intended to let users query the current +/// time. The name "wall" makes an analogy to a "clock on the wall", which +/// is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// A wall clock is a clock which measures the date and time according to +/// some external reference. +/// +/// External references may be reset, so this clock is not necessarily +/// monotonic, making it unsuitable for measuring elapsed time. +/// +/// It is intended for reporting the current date and time for humans. +@since(version = 0.2.0) +interface wall-clock { + /// A time and date in seconds plus nanoseconds. + @since(version = 0.2.0) + record datetime { + seconds: u64, + nanoseconds: u32, + } + + /// Read the current value of the clock. + /// + /// This clock is not monotonic, therefore calling this function repeatedly + /// will not necessarily produce a sequence of non-decreasing values. + /// + /// The returned timestamps represent the number of seconds since + /// 1970-01-01T00:00:00Z, also known as [POSIX's Seconds Since the Epoch], + /// also known as [Unix Time]. + /// + /// The nanoseconds field of the output is always less than 1000000000. + /// + /// [POSIX's Seconds Since the Epoch]: https://pubs.opengroup.org/onlinepubs/9699919799/xrat/V4_xbd_chap04.html#tag_21_04_16 + /// [Unix Time]: https://en.wikipedia.org/wiki/Unix_time + @since(version = 0.2.0) + now: func() -> datetime; + + /// Query the resolution of the clock. + /// + /// The nanoseconds field of the output is always less than 1000000000. + @since(version = 0.2.0) + resolution: func() -> datetime; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/world.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/world.wit new file mode 100644 index 00000000..5c53c51a --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/clocks/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import monotonic-clock; + @since(version = 0.2.0) + import wall-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/preopens.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/preopens.wit new file mode 100644 index 00000000..f2284794 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/preopens.wit @@ -0,0 +1,11 @@ +package wasi:filesystem@0.2.6; + +@since(version = 0.2.0) +interface preopens { + @since(version = 0.2.0) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.2.0) + get-directories: func() -> list>; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/types.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/types.wit new file mode 100644 index 00000000..75c19044 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/types.wit @@ -0,0 +1,676 @@ +package wasi:filesystem@0.2.6; +/// WASI filesystem is a filesystem API primarily intended to let users run WASI +/// programs that access their files on their existing filesystems, without +/// significant overhead. +/// +/// It is intended to be roughly portable between Unix-family platforms and +/// Windows, though it does not hide many of the major differences. +/// +/// Paths are passed as interface-type `string`s, meaning they must consist of +/// a sequence of Unicode Scalar Values (USVs). Some filesystems may contain +/// paths which are not accessible by this API. +/// +/// The directory separator in WASI is always the forward-slash (`/`). +/// +/// All paths in WASI are relative paths, and are interpreted relative to a +/// `descriptor` referring to a base directory. If a `path` argument to any WASI +/// function starts with `/`, or if any step of resolving a `path`, including +/// `..` and symbolic link steps, reaches a directory outside of the base +/// directory, or reaches a symlink to an absolute or rooted path in the +/// underlying filesystem, the function fails with `error-code::not-permitted`. +/// +/// For more information about WASI path resolution and sandboxing, see +/// [WASI filesystem path resolution]. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.2.0) +interface types { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream, output-stream, error}; + @since(version = 0.2.0) + use wasi:clocks/wall-clock@0.2.6.{datetime}; + + /// File size or length of a region within a file. + @since(version = 0.2.0) + type filesize = u64; + + /// The type of a filesystem object referenced by a descriptor. + /// + /// Note: This was called `filetype` in earlier versions of WASI. + @since(version = 0.2.0) + enum descriptor-type { + /// The type of the descriptor or file is unknown or is different from + /// any of the other types specified. + unknown, + /// The descriptor refers to a block device inode. + block-device, + /// The descriptor refers to a character device inode. + character-device, + /// The descriptor refers to a directory inode. + directory, + /// The descriptor refers to a named pipe. + fifo, + /// The file refers to a symbolic link inode. + symbolic-link, + /// The descriptor refers to a regular file inode. + regular-file, + /// The descriptor refers to a socket. + socket, + } + + /// Descriptor flags. + /// + /// Note: This was called `fdflags` in earlier versions of WASI. + @since(version = 0.2.0) + flags descriptor-flags { + /// Read mode: Data can be read. + read, + /// Write mode: Data can be written to. + write, + /// Request that writes be performed according to synchronized I/O file + /// integrity completion. The data stored in the file and the file's + /// metadata are synchronized. This is similar to `O_SYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + file-integrity-sync, + /// Request that writes be performed according to synchronized I/O data + /// integrity completion. Only the data stored in the file is + /// synchronized. This is similar to `O_DSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + data-integrity-sync, + /// Requests that reads be performed at the same level of integrity + /// requested for writes. This is similar to `O_RSYNC` in POSIX. + /// + /// The precise semantics of this operation have not yet been defined for + /// WASI. At this time, it should be interpreted as a request, and not a + /// requirement. + requested-write-sync, + /// Mutating directories mode: Directory contents may be mutated. + /// + /// When this flag is unset on a descriptor, operations using the + /// descriptor which would create, rename, delete, modify the data or + /// metadata of filesystem objects, or obtain another handle which + /// would permit any of those, shall fail with `error-code::read-only` if + /// they would otherwise succeed. + /// + /// This may only be set on directories. + mutate-directory, + } + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.2.0) + record descriptor-stat { + /// File type. + %type: descriptor-type, + /// Number of hard links to the file. + link-count: link-count, + /// For regular files, the file size in bytes. For symbolic links, the + /// length in bytes of the pathname contained in the symbolic link. + size: filesize, + /// Last data access timestamp. + /// + /// If the `option` is none, the platform doesn't maintain an access + /// timestamp for this file. + data-access-timestamp: option, + /// Last data modification timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// modification timestamp for this file. + data-modification-timestamp: option, + /// Last file status-change timestamp. + /// + /// If the `option` is none, the platform doesn't maintain a + /// status-change timestamp for this file. + status-change-timestamp: option, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.2.0) + flags path-flags { + /// As long as the resolved path corresponds to a symbolic link, it is + /// expanded. + symlink-follow, + } + + /// Open flags used by `open-at`. + @since(version = 0.2.0) + flags open-flags { + /// Create file if it does not exist, similar to `O_CREAT` in POSIX. + create, + /// Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + directory, + /// Fail if file already exists, similar to `O_EXCL` in POSIX. + exclusive, + /// Truncate file to size 0, similar to `O_TRUNC` in POSIX. + truncate, + } + + /// Number of hard links to an inode. + @since(version = 0.2.0) + type link-count = u64; + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.2.0) + variant new-timestamp { + /// Leave the timestamp set to its previous value. + no-change, + /// Set the timestamp to the current time of the system clock associated + /// with the filesystem. + now, + /// Set the timestamp to the given value. + timestamp(datetime), + } + + /// A directory entry. + record directory-entry { + /// The type of the file referred to by this directory entry. + %type: descriptor-type, + + /// The name of the object. + name: string, + } + + /// Error codes returned by functions, similar to `errno` in POSIX. + /// Not all of these error codes are returned by the functions provided by this + /// API; some are used in higher-level library layers, and others are provided + /// merely for alignment with POSIX. + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + would-block, + /// Connection already in progress, similar to `EALREADY` in POSIX. + already, + /// Bad descriptor, similar to `EBADF` in POSIX. + bad-descriptor, + /// Device or resource busy, similar to `EBUSY` in POSIX. + busy, + /// Resource deadlock would occur, similar to `EDEADLK` in POSIX. + deadlock, + /// Storage quota exceeded, similar to `EDQUOT` in POSIX. + quota, + /// File exists, similar to `EEXIST` in POSIX. + exist, + /// File too large, similar to `EFBIG` in POSIX. + file-too-large, + /// Illegal byte sequence, similar to `EILSEQ` in POSIX. + illegal-byte-sequence, + /// Operation in progress, similar to `EINPROGRESS` in POSIX. + in-progress, + /// Interrupted function, similar to `EINTR` in POSIX. + interrupted, + /// Invalid argument, similar to `EINVAL` in POSIX. + invalid, + /// I/O error, similar to `EIO` in POSIX. + io, + /// Is a directory, similar to `EISDIR` in POSIX. + is-directory, + /// Too many levels of symbolic links, similar to `ELOOP` in POSIX. + loop, + /// Too many links, similar to `EMLINK` in POSIX. + too-many-links, + /// Message too large, similar to `EMSGSIZE` in POSIX. + message-size, + /// Filename too long, similar to `ENAMETOOLONG` in POSIX. + name-too-long, + /// No such device, similar to `ENODEV` in POSIX. + no-device, + /// No such file or directory, similar to `ENOENT` in POSIX. + no-entry, + /// No locks available, similar to `ENOLCK` in POSIX. + no-lock, + /// Not enough space, similar to `ENOMEM` in POSIX. + insufficient-memory, + /// No space left on device, similar to `ENOSPC` in POSIX. + insufficient-space, + /// Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + not-directory, + /// Directory not empty, similar to `ENOTEMPTY` in POSIX. + not-empty, + /// State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + not-recoverable, + /// Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + unsupported, + /// Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + no-tty, + /// No such device or address, similar to `ENXIO` in POSIX. + no-such-device, + /// Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + overflow, + /// Operation not permitted, similar to `EPERM` in POSIX. + not-permitted, + /// Broken pipe, similar to `EPIPE` in POSIX. + pipe, + /// Read-only file system, similar to `EROFS` in POSIX. + read-only, + /// Invalid seek, similar to `ESPIPE` in POSIX. + invalid-seek, + /// Text file busy, similar to `ETXTBSY` in POSIX. + text-file-busy, + /// Cross-device link, similar to `EXDEV` in POSIX. + cross-device, + } + + /// File or memory access pattern advisory information. + @since(version = 0.2.0) + enum advice { + /// The application has no advice to give on its behavior with respect + /// to the specified data. + normal, + /// The application expects to access the specified data sequentially + /// from lower offsets to higher offsets. + sequential, + /// The application expects to access the specified data in a random + /// order. + random, + /// The application expects to access the specified data in the near + /// future. + will-need, + /// The application expects that it will not access the specified data + /// in the near future. + dont-need, + /// The application expects to access the specified data once and then + /// not reuse it thereafter. + no-reuse, + } + + /// A 128-bit hash value, split into parts because wasm doesn't have a + /// 128-bit integer type. + @since(version = 0.2.0) + record metadata-hash-value { + /// 64 bits of a 128-bit hash value. + lower: u64, + /// Another 64 bits of a 128-bit hash value. + upper: u64, + } + + /// A descriptor is a reference to a filesystem object, which may be a file, + /// directory, named pipe, special file, or other object on which filesystem + /// calls may be made. + @since(version = 0.2.0) + resource descriptor { + /// Return a stream for reading from a file, if available. + /// + /// May fail with an error-code describing why the file cannot be read. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// Note: This allows using `read-stream`, which is similar to `read` in POSIX. + @since(version = 0.2.0) + read-via-stream: func( + /// The offset within the file at which to start reading. + offset: filesize, + ) -> result; + + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// Note: This allows using `write-stream`, which is similar to `write` in + /// POSIX. + @since(version = 0.2.0) + write-via-stream: func( + /// The offset within the file at which to start writing. + offset: filesize, + ) -> result; + + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// Note: This allows using `write-stream`, which is similar to `write` with + /// `O_APPEND` in POSIX. + @since(version = 0.2.0) + append-via-stream: func() -> result; + + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.2.0) + advise: func( + /// The offset within the file to which the advisory applies. + offset: filesize, + /// The length of the region to which the advisory applies. + length: filesize, + /// The advice. + advice: advice + ) -> result<_, error-code>; + + /// Synchronize the data of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fdatasync` in POSIX. + @since(version = 0.2.0) + sync-data: func() -> result<_, error-code>; + + /// Get flags associated with a descriptor. + /// + /// Note: This returns similar flags to `fcntl(fd, F_GETFL)` in POSIX. + /// + /// Note: This returns the value that was the `fs_flags` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) + get-flags: func() -> result; + + /// Get the dynamic type of a descriptor. + /// + /// Note: This returns the same value as the `type` field of the `fd-stat` + /// returned by `stat`, `stat-at` and similar. + /// + /// Note: This returns similar flags to the `st_mode & S_IFMT` value provided + /// by `fstat` in POSIX. + /// + /// Note: This returns the value that was the `fs_filetype` value returned + /// from `fdstat_get` in earlier versions of WASI. + @since(version = 0.2.0) + get-type: func() -> result; + + /// Adjust the size of an open file. If this increases the file's size, the + /// extra bytes are filled with zeros. + /// + /// Note: This was called `fd_filestat_set_size` in earlier versions of WASI. + @since(version = 0.2.0) + set-size: func(size: filesize) -> result<_, error-code>; + + /// Adjust the timestamps of an open file or directory. + /// + /// Note: This is similar to `futimens` in POSIX. + /// + /// Note: This was called `fd_filestat_set_times` in earlier versions of WASI. + @since(version = 0.2.0) + set-times: func( + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Read from a descriptor, without using and updating the descriptor's offset. + /// + /// This function returns a list of bytes containing the data that was + /// read, along with a bool which, when true, indicates that the end of the + /// file was reached. The returned list will contain up to `length` bytes; it + /// may return fewer than requested, if the end of the file is reached or + /// if the I/O operation is interrupted. + /// + /// In the future, this may change to return a `stream`. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read. + length: filesize, + /// The offset within the file at which to read. + offset: filesize, + ) -> result, bool>, error-code>; + + /// Write to a descriptor, without using and updating the descriptor's offset. + /// + /// It is valid to write past the end of a file; the file is extended to the + /// extent of the write, with bytes between the previous end and the start of + /// the write set to zero. + /// + /// In the future, this may change to take a `stream`. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.2.0) + write: func( + /// Data to write + buffer: list, + /// The offset within the file at which to write. + offset: filesize, + ) -> result; + + /// Read directory entries from a directory. + /// + /// On filesystems where directories contain entries referring to themselves + /// and their parents, often named `.` and `..` respectively, these entries + /// are omitted. + /// + /// This always returns a new stream which starts at the beginning of the + /// directory. Multiple streams may be active on the same directory, and they + /// do not interfere with each other. + @since(version = 0.2.0) + read-directory: func() -> result; + + /// Synchronize the data and metadata of a file to disk. + /// + /// This function succeeds with no effect if the file descriptor is not + /// opened for writing. + /// + /// Note: This is similar to `fsync` in POSIX. + @since(version = 0.2.0) + sync: func() -> result<_, error-code>; + + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.2.0) + create-directory-at: func( + /// The relative path at which to create the directory. + path: string, + ) -> result<_, error-code>; + + /// Return the attributes of an open file or directory. + /// + /// Note: This is similar to `fstat` in POSIX, except that it does not return + /// device and inode information. For testing whether two descriptors refer to + /// the same underlying filesystem object, use `is-same-object`. To obtain + /// additional data that can be used do determine whether a file has been + /// modified, use `metadata-hash`. + /// + /// Note: This was called `fd_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) + stat: func() -> result; + + /// Return the attributes of a file or directory. + /// + /// Note: This is similar to `fstatat` in POSIX, except that it does not + /// return device and inode information. See the `stat` description for a + /// discussion of alternatives. + /// + /// Note: This was called `path_filestat_get` in earlier versions of WASI. + @since(version = 0.2.0) + stat-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + + /// Adjust the timestamps of a file or directory. + /// + /// Note: This is similar to `utimensat` in POSIX. + /// + /// Note: This was called `path_filestat_set_times` in earlier versions of + /// WASI. + @since(version = 0.2.0) + set-times-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to operate on. + path: string, + /// The desired values of the data access timestamp. + data-access-timestamp: new-timestamp, + /// The desired values of the data modification timestamp. + data-modification-timestamp: new-timestamp, + ) -> result<_, error-code>; + + /// Create a hard link. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.2.0) + link-at: func( + /// Flags determining the method of how the path is resolved. + old-path-flags: path-flags, + /// The relative source path from which to link. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path at which to create the hard link. + new-path: string, + ) -> result<_, error-code>; + + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.2.0) + open-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the object to open. + path: string, + /// The method by which to open the file. + open-flags: open-flags, + /// Flags to use for the resulting descriptor. + %flags: descriptor-flags, + ) -> result; + + /// Read the contents of a symbolic link. + /// + /// If the contents contain an absolute or rooted path in the underlying + /// filesystem, this function fails with `error-code::not-permitted`. + /// + /// Note: This is similar to `readlinkat` in POSIX. + @since(version = 0.2.0) + readlink-at: func( + /// The relative path of the symbolic link from which to read. + path: string, + ) -> result; + + /// Remove a directory. + /// + /// Return `error-code::not-empty` if the directory is not empty. + /// + /// Note: This is similar to `unlinkat(fd, path, AT_REMOVEDIR)` in POSIX. + @since(version = 0.2.0) + remove-directory-at: func( + /// The relative path to a directory to remove. + path: string, + ) -> result<_, error-code>; + + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.2.0) + rename-at: func( + /// The relative source path of the file or directory to rename. + old-path: string, + /// The base directory for `new-path`. + new-descriptor: borrow, + /// The relative destination path to which to rename the file or directory. + new-path: string, + ) -> result<_, error-code>; + + /// Create a symbolic link (also known as a "symlink"). + /// + /// If `old-path` starts with `/`, the function fails with + /// `error-code::not-permitted`. + /// + /// Note: This is similar to `symlinkat` in POSIX. + @since(version = 0.2.0) + symlink-at: func( + /// The contents of the symbolic link. + old-path: string, + /// The relative destination path at which to create the symbolic link. + new-path: string, + ) -> result<_, error-code>; + + /// Unlink a filesystem object that is not a directory. + /// + /// Return `error-code::is-directory` if the path refers to a directory. + /// Note: This is similar to `unlinkat(fd, path, 0)` in POSIX. + @since(version = 0.2.0) + unlink-file-at: func( + /// The relative path to a file to unlink. + path: string, + ) -> result<_, error-code>; + + /// Test whether two descriptors refer to the same filesystem object. + /// + /// In POSIX, this corresponds to testing whether the two descriptors have the + /// same device (`st_dev`) and inode (`st_ino` or `d_ino`) numbers. + /// wasi-filesystem does not expose device and inode numbers, so this function + /// may be used instead. + @since(version = 0.2.0) + is-same-object: func(other: borrow) -> bool; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.2.0) + metadata-hash: func() -> result; + + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a directory descriptor and a relative path. + /// + /// This performs the same hash computation as `metadata-hash`. + @since(version = 0.2.0) + metadata-hash-at: func( + /// Flags determining the method of how the path is resolved. + path-flags: path-flags, + /// The relative path of the file or directory to inspect. + path: string, + ) -> result; + } + + /// A stream of directory entries. + @since(version = 0.2.0) + resource directory-entry-stream { + /// Read a single directory entry from a `directory-entry-stream`. + @since(version = 0.2.0) + read-directory-entry: func() -> result, error-code>; + } + + /// Attempts to extract a filesystem-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// filesystem-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are filesystem-related errors. + @since(version = 0.2.0) + filesystem-error-code: func(err: borrow) -> option; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/world.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/world.wit new file mode 100644 index 00000000..65597f9f --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/filesystem/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import types; + @since(version = 0.2.0) + import preopens; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/http/handler.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/http/handler.wit new file mode 100644 index 00000000..6a6c6296 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/http/handler.wit @@ -0,0 +1,49 @@ +/// This interface defines a handler of incoming HTTP Requests. It should +/// be exported by components which can respond to HTTP Requests. +@since(version = 0.2.0) +interface incoming-handler { + @since(version = 0.2.0) + use types.{incoming-request, response-outparam}; + + /// This function is invoked with an incoming HTTP Request, and a resource + /// `response-outparam` which provides the capability to reply with an HTTP + /// Response. The response is sent by calling the `response-outparam.set` + /// method, which allows execution to continue after the response has been + /// sent. This enables both streaming to the response body, and performing other + /// work. + /// + /// The implementor of this function must write a response to the + /// `response-outparam` before returning, or else the caller will respond + /// with an error on its behalf. + @since(version = 0.2.0) + handle: func( + request: incoming-request, + response-out: response-outparam + ); +} + +/// This interface defines a handler of outgoing HTTP Requests. It should be +/// imported by components which wish to make HTTP Requests. +@since(version = 0.2.0) +interface outgoing-handler { + @since(version = 0.2.0) + use types.{ + outgoing-request, request-options, future-incoming-response, error-code + }; + + /// This function is invoked with an outgoing HTTP Request, and it returns + /// a resource `future-incoming-response` which represents an HTTP Response + /// which may arrive in the future. + /// + /// The `options` argument accepts optional parameters for the HTTP + /// protocol's transport layer. + /// + /// This function may return an error if the `outgoing-request` is invalid + /// or not allowed to be made. Otherwise, protocol errors are reported + /// through the `future-incoming-response`. + @since(version = 0.2.0) + handle: func( + request: outgoing-request, + options: option + ) -> result; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/http/proxy.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/http/proxy.wit new file mode 100644 index 00000000..5bd9f998 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/http/proxy.wit @@ -0,0 +1,50 @@ +package wasi:http@0.2.6; + +/// The `wasi:http/imports` world imports all the APIs for HTTP proxies. +/// It is intended to be `include`d in other worlds. +@since(version = 0.2.0) +world imports { + /// HTTP proxies have access to time and randomness. + @since(version = 0.2.0) + import wasi:clocks/monotonic-clock@0.2.6; + @since(version = 0.2.0) + import wasi:clocks/wall-clock@0.2.6; + @since(version = 0.2.0) + import wasi:random/random@0.2.6; + + /// Proxies have standard output and error streams which are expected to + /// terminate in a developer-facing console provided by the host. + @since(version = 0.2.0) + import wasi:cli/stdout@0.2.6; + @since(version = 0.2.0) + import wasi:cli/stderr@0.2.6; + + /// TODO: this is a temporary workaround until component tooling is able to + /// gracefully handle the absence of stdin. Hosts must return an eof stream + /// for this import, which is what wasi-libc + tooling will do automatically + /// when this import is properly removed. + @since(version = 0.2.0) + import wasi:cli/stdin@0.2.6; + + /// This is the default handler to use when user code simply wants to make an + /// HTTP request (e.g., via `fetch()`). + @since(version = 0.2.0) + import outgoing-handler; +} + +/// The `wasi:http/proxy` world captures a widely-implementable intersection of +/// hosts that includes HTTP forward and reverse proxies. Components targeting +/// this world may concurrently stream in and out any number of incoming and +/// outgoing HTTP requests. +@since(version = 0.2.0) +world proxy { + @since(version = 0.2.0) + include imports; + + /// The host delivers incoming HTTP requests to a component by calling the + /// `handle` function of this exported interface. A host may arbitrarily reuse + /// or not reuse component instance when delivering incoming HTTP requests and + /// thus a component must be able to handle 0..N calls to `handle`. + @since(version = 0.2.0) + export incoming-handler; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/http/types.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/http/types.wit new file mode 100644 index 00000000..e1f26e6b --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/http/types.wit @@ -0,0 +1,688 @@ +/// This interface defines all of the types and methods for implementing +/// HTTP Requests and Responses, both incoming and outgoing, as well as +/// their headers, trailers, and bodies. +@since(version = 0.2.0) +interface types { + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.6.{duration}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/error@0.2.6.{error as io-error}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + + /// This type corresponds to HTTP standard Methods. + @since(version = 0.2.0) + variant method { + get, + head, + post, + put, + delete, + connect, + options, + trace, + patch, + other(string) + } + + /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.2.0) + variant scheme { + HTTP, + HTTPS, + other(string) + } + + /// These cases are inspired by the IANA HTTP Proxy Error Types: + /// + @since(version = 0.2.0) + variant error-code { + DNS-timeout, + DNS-error(DNS-error-payload), + destination-not-found, + destination-unavailable, + destination-IP-prohibited, + destination-IP-unroutable, + connection-refused, + connection-terminated, + connection-timeout, + connection-read-timeout, + connection-write-timeout, + connection-limit-reached, + TLS-protocol-error, + TLS-certificate-error, + TLS-alert-received(TLS-alert-received-payload), + HTTP-request-denied, + HTTP-request-length-required, + HTTP-request-body-size(option), + HTTP-request-method-invalid, + HTTP-request-URI-invalid, + HTTP-request-URI-too-long, + HTTP-request-header-section-size(option), + HTTP-request-header-size(option), + HTTP-request-trailer-section-size(option), + HTTP-request-trailer-size(field-size-payload), + HTTP-response-incomplete, + HTTP-response-header-section-size(option), + HTTP-response-header-size(field-size-payload), + HTTP-response-body-size(option), + HTTP-response-trailer-section-size(option), + HTTP-response-trailer-size(field-size-payload), + HTTP-response-transfer-coding(option), + HTTP-response-content-coding(option), + HTTP-response-timeout, + HTTP-upgrade-failed, + HTTP-protocol-error, + loop-detected, + configuration-error, + /// This is a catch-all error for anything that doesn't fit cleanly into a + /// more specific case. It also includes an optional string for an + /// unstructured description of the error. Users should not depend on the + /// string for diagnosing errors, as it's not required to be consistent + /// between implementations. + internal-error(option) + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.2.0) + record DNS-error-payload { + rcode: option, + info-code: option + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.2.0) + record TLS-alert-received-payload { + alert-id: option, + alert-message: option + } + + /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + @since(version = 0.2.0) + record field-size-payload { + field-name: option, + field-size: option + } + + /// Attempts to extract a http-related `error` from the wasi:io `error` + /// provided. + /// + /// Stream operations which return + /// `wasi:io/stream/stream-error::last-operation-failed` have a payload of + /// type `wasi:io/error/error` with more information about the operation + /// that failed. This payload can be passed through to this function to see + /// if there's http-related information about the error to return. + /// + /// Note that this function is fallible because not all io-errors are + /// http-related errors. + @since(version = 0.2.0) + http-error-code: func(err: borrow) -> option; + + /// This type enumerates the different kinds of errors that may occur when + /// setting or appending to a `fields` resource. + @since(version = 0.2.0) + variant header-error { + /// This error indicates that a `field-name` or `field-value` was + /// syntactically invalid when used with an operation that sets headers in a + /// `fields`. + invalid-syntax, + + /// This error indicates that a forbidden `field-name` was used when trying + /// to set a header in a `fields`. + forbidden, + + /// This error indicates that the operation on the `fields` was not + /// permitted because the fields are immutable. + immutable, + } + + /// Field names are always strings. + /// + /// Field names should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + @since(version = 0.2.1) + type field-name = field-key; + + /// Field keys are always strings. + /// + /// Field keys should always be treated as case insensitive by the `fields` + /// resource for the purposes of equality checking. + /// + /// # Deprecation + /// + /// This type has been deprecated in favor of the `field-name` type. + @since(version = 0.2.0) + @deprecated(version = 0.2.2) + type field-key = string; + + /// Field values should always be ASCII strings. However, in + /// reality, HTTP implementations often have to interpret malformed values, + /// so they are provided as a list of bytes. + @since(version = 0.2.0) + type field-value = list; + + /// This following block defines the `fields` resource which corresponds to + /// HTTP standard Fields. Fields are a common representation used for both + /// Headers and Trailers. + /// + /// A `fields` may be mutable or immutable. A `fields` created using the + /// constructor, `from-list`, or `clone` will be mutable, but a `fields` + /// resource given by other means (including, but not limited to, + /// `incoming-request.headers`, `outgoing-request.headers`) might be + /// immutable. In an immutable fields, the `set`, `append`, and `delete` + /// operations will fail with `header-error.immutable`. + @since(version = 0.2.0) + resource fields { + + /// Construct an empty HTTP Fields. + /// + /// The resulting `fields` is mutable. + @since(version = 0.2.0) + constructor(); + + /// Construct an HTTP Fields. + /// + /// The resulting `fields` is mutable. + /// + /// The list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The tuple is a pair of the field name, represented as a string, and + /// Value, represented as a list of bytes. + /// + /// An error result will be returned if any `field-name` or `field-value` is + /// syntactically invalid, or if a field is forbidden. + @since(version = 0.2.0) + from-list: static func( + entries: list> + ) -> result; + + /// Get all of the values corresponding to a name. If the name is not present + /// in this `fields` or is syntactically invalid, an empty list is returned. + /// However, if the name is present but empty, this is represented by a list + /// with one or more empty field-values present. + @since(version = 0.2.0) + get: func(name: field-name) -> list; + + /// Returns `true` when the name is present in this `fields`. If the name is + /// syntactically invalid, `false` is returned. + @since(version = 0.2.0) + has: func(name: field-name) -> bool; + + /// Set all of the values for a name. Clears any existing values for that + /// name, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` or any of + /// the `field-value`s are syntactically invalid. + @since(version = 0.2.0) + set: func(name: field-name, value: list) -> result<_, header-error>; + + /// Delete all values for a name. Does nothing if no values for the name + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` is + /// syntactically invalid. + @since(version = 0.2.0) + delete: func(name: field-name) -> result<_, header-error>; + + /// Append a value for a name. Does not change or delete any existing + /// values for that name. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-name` or + /// `field-value` are syntactically invalid. + @since(version = 0.2.0) + append: func(name: field-name, value: field-value) -> result<_, header-error>; + + /// Retrieve the full set of names and values in the Fields. Like the + /// constructor, the list represents each name-value pair. + /// + /// The outer list represents each name-value pair in the Fields. Names + /// which have multiple values are represented by multiple entries in this + /// list with the same name. + /// + /// The names and values are always returned in the original casing and in + /// the order in which they will be serialized for transport. + @since(version = 0.2.0) + entries: func() -> list>; + + /// Make a deep copy of the Fields. Equivalent in behavior to calling the + /// `fields` constructor on the return value of `entries`. The resulting + /// `fields` is mutable. + @since(version = 0.2.0) + clone: func() -> fields; + } + + /// Headers is an alias for Fields. + @since(version = 0.2.0) + type headers = fields; + + /// Trailers is an alias for Fields. + @since(version = 0.2.0) + type trailers = fields; + + /// Represents an incoming HTTP Request. + @since(version = 0.2.0) + resource incoming-request { + + /// Returns the method of the incoming request. + @since(version = 0.2.0) + method: func() -> method; + + /// Returns the path with query parameters from the request, as a string. + @since(version = 0.2.0) + path-with-query: func() -> option; + + /// Returns the protocol scheme from the request. + @since(version = 0.2.0) + scheme: func() -> option; + + /// Returns the authority of the Request's target URI, if present. + @since(version = 0.2.0) + authority: func() -> option; + + /// Get the `headers` associated with the request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// The `headers` returned are a child resource: it must be dropped before + /// the parent `incoming-request` is dropped. Dropping this + /// `incoming-request` before all children are dropped will trap. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Gives the `incoming-body` associated with this request. Will only + /// return success at most once, and subsequent calls will return error. + @since(version = 0.2.0) + consume: func() -> result; + } + + /// Represents an outgoing HTTP Request. + @since(version = 0.2.0) + resource outgoing-request { + + /// Construct a new `outgoing-request` with a default `method` of `GET`, and + /// `none` values for `path-with-query`, `scheme`, and `authority`. + /// + /// * `headers` is the HTTP Headers for the Request. + /// + /// It is possible to construct, or manipulate with the accessor functions + /// below, an `outgoing-request` with an invalid combination of `scheme` + /// and `authority`, or `headers` which are not permitted to be sent. + /// It is the obligation of the `outgoing-handler.handle` implementation + /// to reject invalid constructions of `outgoing-request`. + @since(version = 0.2.0) + constructor( + headers: headers + ); + + /// Returns the resource corresponding to the outgoing Body for this + /// Request. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-request` can be retrieved at most once. Subsequent + /// calls will return error. + @since(version = 0.2.0) + body: func() -> result; + + /// Get the Method for the Request. + @since(version = 0.2.0) + method: func() -> method; + /// Set the Method for the Request. Fails if the string present in a + /// `method.other` argument is not a syntactically valid method. + @since(version = 0.2.0) + set-method: func(method: method) -> result; + + /// Get the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. + @since(version = 0.2.0) + path-with-query: func() -> option; + /// Set the combination of the HTTP Path and Query for the Request. + /// When `none`, this represents an empty Path and empty Query. Fails is the + /// string given is not a syntactically valid path and query uri component. + @since(version = 0.2.0) + set-path-with-query: func(path-with-query: option) -> result; + + /// Get the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. + @since(version = 0.2.0) + scheme: func() -> option; + /// Set the HTTP Related Scheme for the Request. When `none`, the + /// implementation may choose an appropriate default scheme. Fails if the + /// string given is not a syntactically valid uri scheme. + @since(version = 0.2.0) + set-scheme: func(scheme: option) -> result; + + /// Get the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. + @since(version = 0.2.0) + authority: func() -> option; + /// Set the authority of the Request's target URI. A value of `none` may be used + /// with Related Schemes which do not require an authority. The HTTP and + /// HTTPS schemes always require an authority. Fails if the string given is + /// not a syntactically valid URI authority. + @since(version = 0.2.0) + set-authority: func(authority: option) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transferred to + /// another component by e.g. `outgoing-handler.handle`. + @since(version = 0.2.0) + headers: func() -> headers; + } + + /// Parameters for making an HTTP Request. Each of these parameters is + /// currently an optional timeout applicable to the transport layer of the + /// HTTP protocol. + /// + /// These timeouts are separate from any the user may use to bound a + /// blocking call to `wasi:io/poll.poll`. + @since(version = 0.2.0) + resource request-options { + /// Construct a default `request-options` value. + @since(version = 0.2.0) + constructor(); + + /// The timeout for the initial connect to the HTTP Server. + @since(version = 0.2.0) + connect-timeout: func() -> option; + + /// Set the timeout for the initial connect to the HTTP Server. An error + /// return value indicates that this timeout is not supported. + @since(version = 0.2.0) + set-connect-timeout: func(duration: option) -> result; + + /// The timeout for receiving the first byte of the Response body. + @since(version = 0.2.0) + first-byte-timeout: func() -> option; + + /// Set the timeout for receiving the first byte of the Response body. An + /// error return value indicates that this timeout is not supported. + @since(version = 0.2.0) + set-first-byte-timeout: func(duration: option) -> result; + + /// The timeout for receiving subsequent chunks of bytes in the Response + /// body stream. + @since(version = 0.2.0) + between-bytes-timeout: func() -> option; + + /// Set the timeout for receiving subsequent chunks of bytes in the Response + /// body stream. An error return value indicates that this timeout is not + /// supported. + @since(version = 0.2.0) + set-between-bytes-timeout: func(duration: option) -> result; + } + + /// Represents the ability to send an HTTP Response. + /// + /// This resource is used by the `wasi:http/incoming-handler` interface to + /// allow a Response to be sent corresponding to the Request provided as the + /// other argument to `incoming-handler.handle`. + @since(version = 0.2.0) + resource response-outparam { + /// Send an HTTP 1xx response. + /// + /// Unlike `response-outparam.set`, this does not consume the + /// `response-outparam`, allowing the guest to send an arbitrary number of + /// informational responses before sending the final response using + /// `response-outparam.set`. + /// + /// This will return an `HTTP-protocol-error` if `status` is not in the + /// range [100-199], or an `internal-error` if the implementation does not + /// support informational responses. + @unstable(feature = informational-outbound-responses) + send-informational: func( + status: u16, + headers: headers + ) -> result<_, error-code>; + + /// Set the value of the `response-outparam` to either send a response, + /// or indicate an error. + /// + /// This method consumes the `response-outparam` to ensure that it is + /// called at most once. If it is never called, the implementation + /// will respond with an error. + /// + /// The user may provide an `error` to `response` to allow the + /// implementation determine how to respond with an HTTP error response. + @since(version = 0.2.0) + set: static func( + param: response-outparam, + response: result, + ); + } + + /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.2.0) + type status-code = u16; + + /// Represents an incoming HTTP Response. + @since(version = 0.2.0) + resource incoming-response { + + /// Returns the status code from the incoming response. + @since(version = 0.2.0) + status: func() -> status-code; + + /// Returns the headers from the incoming response. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `incoming-response` is dropped. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Returns the incoming body. May be called at most once. Returns error + /// if called additional times. + @since(version = 0.2.0) + consume: func() -> result; + } + + /// Represents an incoming HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, indicating that the full contents of the + /// body have been received. This resource represents the contents as + /// an `input-stream` and the delivery of trailers as a `future-trailers`, + /// and ensures that the user of this interface may only be consuming either + /// the body contents or waiting on trailers at any given time. + @since(version = 0.2.0) + resource incoming-body { + + /// Returns the contents of the body, as a stream of bytes. + /// + /// Returns success on first call: the stream representing the contents + /// can be retrieved at most once. Subsequent calls will return error. + /// + /// The returned `input-stream` resource is a child: it must be dropped + /// before the parent `incoming-body` is dropped, or consumed by + /// `incoming-body.finish`. + /// + /// This invariant ensures that the implementation can determine whether + /// the user is consuming the contents of the body, waiting on the + /// `future-trailers` to be ready, or neither. This allows for network + /// backpressure is to be applied when the user is consuming the body, + /// and for that backpressure to not inhibit delivery of the trailers if + /// the user does not read the entire body. + @since(version = 0.2.0) + %stream: func() -> result; + + /// Takes ownership of `incoming-body`, and returns a `future-trailers`. + /// This function will trap if the `input-stream` child is still alive. + @since(version = 0.2.0) + finish: static func(this: incoming-body) -> future-trailers; + } + + /// Represents a future which may eventually return trailers, or an error. + /// + /// In the case that the incoming HTTP Request or Response did not have any + /// trailers, this future will resolve to the empty set of trailers once the + /// complete Request or Response body has been received. + @since(version = 0.2.0) + resource future-trailers { + + /// Returns a pollable which becomes ready when either the trailers have + /// been received, or an error has occurred. When this pollable is ready, + /// the `get` method will return `some`. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Returns the contents of the trailers, or an error which occurred, + /// once the future is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the trailers or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the HTTP Request or Response + /// body, as well as any trailers, were received successfully, or that an + /// error occurred receiving them. The optional `trailers` indicates whether + /// or not trailers were present in the body. + /// + /// When some `trailers` are returned by this method, the `trailers` + /// resource is immutable, and a child. Use of the `set`, `append`, or + /// `delete` methods will return an error, and the resource must be + /// dropped before the parent `future-trailers` is dropped. + @since(version = 0.2.0) + get: func() -> option, error-code>>>; + } + + /// Represents an outgoing HTTP Response. + @since(version = 0.2.0) + resource outgoing-response { + + /// Construct an `outgoing-response`, with a default `status-code` of `200`. + /// If a different `status-code` is needed, it must be set via the + /// `set-status-code` method. + /// + /// * `headers` is the HTTP Headers for the Response. + @since(version = 0.2.0) + constructor(headers: headers); + + /// Get the HTTP Status Code for the Response. + @since(version = 0.2.0) + status-code: func() -> status-code; + + /// Set the HTTP Status Code for the Response. Fails if the status-code + /// given is not a valid http status code. + @since(version = 0.2.0) + set-status-code: func(status-code: status-code) -> result; + + /// Get the headers associated with the Request. + /// + /// The returned `headers` resource is immutable: `set`, `append`, and + /// `delete` operations will fail with `header-error.immutable`. + /// + /// This headers resource is a child: it must be dropped before the parent + /// `outgoing-request` is dropped, or its ownership is transferred to + /// another component by e.g. `outgoing-handler.handle`. + @since(version = 0.2.0) + headers: func() -> headers; + + /// Returns the resource corresponding to the outgoing Body for this Response. + /// + /// Returns success on the first call: the `outgoing-body` resource for + /// this `outgoing-response` can be retrieved at most once. Subsequent + /// calls will return error. + @since(version = 0.2.0) + body: func() -> result; + } + + /// Represents an outgoing HTTP Request or Response's Body. + /// + /// A body has both its contents - a stream of bytes - and a (possibly + /// empty) set of trailers, inducating the full contents of the body + /// have been sent. This resource represents the contents as an + /// `output-stream` child resource, and the completion of the body (with + /// optional trailers) with a static function that consumes the + /// `outgoing-body` resource, and ensures that the user of this interface + /// may not write to the body contents after the body has been finished. + /// + /// If the user code drops this resource, as opposed to calling the static + /// method `finish`, the implementation should treat the body as incomplete, + /// and that an error has occurred. The implementation should propagate this + /// error to the HTTP protocol by whatever means it has available, + /// including: corrupting the body on the wire, aborting the associated + /// Request, or sending a late status code for the Response. + @since(version = 0.2.0) + resource outgoing-body { + + /// Returns a stream for writing the body contents. + /// + /// The returned `output-stream` is a child resource: it must be dropped + /// before the parent `outgoing-body` resource is dropped (or finished), + /// otherwise the `outgoing-body` drop or `finish` will trap. + /// + /// Returns success on the first call: the `output-stream` resource for + /// this `outgoing-body` may be retrieved at most once. Subsequent calls + /// will return error. + @since(version = 0.2.0) + write: func() -> result; + + /// Finalize an outgoing body, optionally providing trailers. This must be + /// called to signal that the response is complete. If the `outgoing-body` + /// is dropped without calling `outgoing-body.finalize`, the implementation + /// should treat the body as corrupted. + /// + /// Fails if the body's `outgoing-request` or `outgoing-response` was + /// constructed with a Content-Length header, and the contents written + /// to the body (via `write`) does not match the value given in the + /// Content-Length. + @since(version = 0.2.0) + finish: static func( + this: outgoing-body, + trailers: option + ) -> result<_, error-code>; + } + + /// Represents a future which may eventually return an incoming HTTP + /// Response, or an error. + /// + /// This resource is returned by the `wasi:http/outgoing-handler` interface to + /// provide the HTTP Response corresponding to the sent Request. + @since(version = 0.2.0) + resource future-incoming-response { + /// Returns a pollable which becomes ready when either the Response has + /// been received, or an error has occurred. When this pollable is ready, + /// the `get` method will return `some`. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Returns the incoming HTTP Response, or an error, once one is ready. + /// + /// The outer `option` represents future readiness. Users can wait on this + /// `option` to become `some` using the `subscribe` method. + /// + /// The outer `result` is used to retrieve the response or error at most + /// once. It will be success on the first call in which the outer option + /// is `some`, and error on subsequent calls. + /// + /// The inner `result` represents that either the incoming HTTP Response + /// status and headers have received successfully, or that an error + /// occurred. Errors may also occur while consuming the response body, + /// but those will be reported by the `incoming-body` and its + /// `output-stream` child. + @since(version = 0.2.0) + get: func() -> option>>; + } +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/io/error.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/io/error.wit new file mode 100644 index 00000000..784f74a5 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.6; + +@since(version = 0.2.0) +interface error { + /// A resource which represents some error information. + /// + /// The only method provided by this resource is `to-debug-string`, + /// which provides some human-readable information about the error. + /// + /// In the `wasi:io` package, this resource is returned through the + /// `wasi:io/streams/stream-error` type. + /// + /// To provide more specific error information, other interfaces may + /// offer functions to "downcast" this error into more specific types. For example, + /// errors returned from streams derived from filesystem types can be described using + /// the filesystem's own error-code type. This is done using the function + /// `wasi:filesystem/types/filesystem-error-code`, which takes a `borrow` + /// parameter and returns an `option`. + /// + /// The set of functions which can "downcast" an `error` into a more + /// concrete type is open. + @since(version = 0.2.0) + resource error { + /// Returns a string that is suitable to assist humans in debugging + /// this error. + /// + /// WARNING: The returned string should not be consumed mechanically! + /// It may change across platforms, hosts, or other implementation + /// details. Parsing this string is a major platform-compatibility + /// hazard. + @since(version = 0.2.0) + to-debug-string: func() -> string; + } +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/io/poll.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/io/poll.wit new file mode 100644 index 00000000..7f711836 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.6; + +/// A poll API intended to let users wait for I/O events on multiple handles +/// at once. +@since(version = 0.2.0) +interface poll { + /// `pollable` represents a single I/O event which may be ready, or not. + @since(version = 0.2.0) + resource pollable { + + /// Return the readiness of a pollable. This function never blocks. + /// + /// Returns `true` when the pollable is ready, and `false` otherwise. + @since(version = 0.2.0) + ready: func() -> bool; + + /// `block` returns immediately if the pollable is ready, and otherwise + /// blocks until ready. + /// + /// This function is equivalent to calling `poll.poll` on a list + /// containing only this pollable. + @since(version = 0.2.0) + block: func(); + } + + /// Poll for completion on a set of pollables. + /// + /// This function takes a list of pollables, which identify I/O sources of + /// interest, and waits until one or more of the events is ready for I/O. + /// + /// The result `list` contains one or more indices of handles in the + /// argument list that is ready for I/O. + /// + /// This function traps if either: + /// - the list is empty, or: + /// - the list contains more elements than can be indexed with a `u32` value. + /// + /// A timeout can be implemented by adding a pollable from the + /// wasi-clocks API to the list. + /// + /// This function does not return a `result`; polling in itself does not + /// do any I/O so it doesn't fail. If any of the I/O sources identified by + /// the pollables has an error, it is indicated by marking the source as + /// being ready for I/O. + @since(version = 0.2.0) + poll: func(in: list>) -> list; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/io/streams.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/io/streams.wit new file mode 100644 index 00000000..c5da38c8 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/io/streams.wit @@ -0,0 +1,290 @@ +package wasi:io@0.2.6; + +/// WASI I/O is an I/O abstraction API which is currently focused on providing +/// stream types. +/// +/// In the future, the component model is expected to add built-in stream types; +/// when it does, they are expected to subsume this API. +@since(version = 0.2.0) +interface streams { + @since(version = 0.2.0) + use error.{error}; + @since(version = 0.2.0) + use poll.{pollable}; + + /// An error for input-stream and output-stream operations. + @since(version = 0.2.0) + variant stream-error { + /// The last operation (a write or flush) failed before completion. + /// + /// More information is available in the `error` payload. + /// + /// After this, the stream will be closed. All future operations return + /// `stream-error::closed`. + last-operation-failed(error), + /// The stream is closed: no more input will be accepted by the + /// stream. A closed output-stream will return this error on all + /// future operations. + closed + } + + /// An input bytestream. + /// + /// `input-stream`s are *non-blocking* to the extent practical on underlying + /// platforms. I/O operations always return promptly; if fewer bytes are + /// promptly available than requested, they return the number of bytes promptly + /// available, which could even be zero. To wait for data to be available, + /// use the `subscribe` function to obtain a `pollable` which can be polled + /// for using `wasi:io/poll`. + @since(version = 0.2.0) + resource input-stream { + /// Perform a non-blocking read from the stream. + /// + /// When the source of a `read` is binary data, the bytes from the source + /// are returned verbatim. When the source of a `read` is known to the + /// implementation to be text, bytes containing the UTF-8 encoding of the + /// text are returned. + /// + /// This function returns a list of bytes containing the read data, + /// when successful. The returned list will contain up to `len` bytes; + /// it may return fewer than requested, but not more. The list is + /// empty when no bytes are available for reading at this time. The + /// pollable given by `subscribe` will be ready when more bytes are + /// available. + /// + /// This function fails with a `stream-error` when the operation + /// encounters an error, giving `last-operation-failed`, or when the + /// stream is closed, giving `closed`. + /// + /// When the caller gives a `len` of 0, it represents a request to + /// read 0 bytes. If the stream is still open, this call should + /// succeed and return an empty list, or otherwise fail with `closed`. + /// + /// The `len` parameter is a `u64`, which could represent a list of u8 which + /// is not possible to allocate in wasm32, or not desirable to allocate as + /// as a return value by the callee. The callee may return a list of bytes + /// less than `len` in size while more bytes are available for reading. + @since(version = 0.2.0) + read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Read bytes from a stream, after blocking until at least one byte can + /// be read. Except for blocking, behavior is identical to `read`. + @since(version = 0.2.0) + blocking-read: func( + /// The maximum number of bytes to read + len: u64 + ) -> result, stream-error>; + + /// Skip bytes from a stream. Returns number of bytes skipped. + /// + /// Behaves identical to `read`, except instead of returning a list + /// of bytes, returns the number of bytes consumed from the stream. + @since(version = 0.2.0) + skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Skip bytes from a stream, after blocking until at least one byte + /// can be skipped. Except for blocking behavior, identical to `skip`. + @since(version = 0.2.0) + blocking-skip: func( + /// The maximum number of bytes to skip. + len: u64, + ) -> result; + + /// Create a `pollable` which will resolve once either the specified stream + /// has bytes available to read or the other end of the stream has been + /// closed. + /// The created `pollable` is a child resource of the `input-stream`. + /// Implementations may trap if the `input-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + + /// An output bytestream. + /// + /// `output-stream`s are *non-blocking* to the extent practical on + /// underlying platforms. Except where specified otherwise, I/O operations also + /// always return promptly, after the number of bytes that can be written + /// promptly, which could even be zero. To wait for the stream to be ready to + /// accept data, the `subscribe` function to obtain a `pollable` which can be + /// polled for using `wasi:io/poll`. + /// + /// Dropping an `output-stream` while there's still an active write in + /// progress may result in the data being lost. Before dropping the stream, + /// be sure to fully flush your writes. + @since(version = 0.2.0) + resource output-stream { + /// Check readiness for writing. This function never blocks. + /// + /// Returns the number of bytes permitted for the next call to `write`, + /// or an error. Calling `write` with more bytes than this function has + /// permitted will trap. + /// + /// When this function returns 0 bytes, the `subscribe` pollable will + /// become ready when this function will report at least 1 byte, or an + /// error. + @since(version = 0.2.0) + check-write: func() -> result; + + /// Perform a write. This function never blocks. + /// + /// When the destination of a `write` is binary data, the bytes from + /// `contents` are written verbatim. When the destination of a `write` is + /// known to the implementation to be text, the bytes of `contents` are + /// transcoded from UTF-8 into the encoding of the destination and then + /// written. + /// + /// Precondition: check-write gave permit of Ok(n) and contents has a + /// length of less than or equal to n. Otherwise, this function will trap. + /// + /// returns Err(closed) without writing if the stream has closed since + /// the last call to check-write provided a permit. + @since(version = 0.2.0) + write: func( + contents: list + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-and-flush: func( + contents: list + ) -> result<_, stream-error>; + + /// Request to flush buffered output. This function never blocks. + /// + /// This tells the output-stream that the caller intends any buffered + /// output to be flushed. the output which is expected to be flushed + /// is all that has been passed to `write` prior to this call. + /// + /// Upon calling this function, the `output-stream` will not accept any + /// writes (`check-write` will return `ok(0)`) until the flush has + /// completed. The `subscribe` pollable will become ready when the + /// flush has completed and the stream can accept more writes. + @since(version = 0.2.0) + flush: func() -> result<_, stream-error>; + + /// Request to flush buffered output, and block until flush completes + /// and stream is ready for writing again. + @since(version = 0.2.0) + blocking-flush: func() -> result<_, stream-error>; + + /// Create a `pollable` which will resolve once the output-stream + /// is ready for more writing, or an error has occurred. When this + /// pollable is ready, `check-write` will return `ok(n)` with n>0, or an + /// error. + /// + /// If the stream is closed, this pollable is always ready immediately. + /// + /// The created `pollable` is a child resource of the `output-stream`. + /// Implementations may trap if the `output-stream` is dropped before + /// all derived `pollable`s created with this function are dropped. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Write zeroes to a stream. + /// + /// This should be used precisely like `write` with the exact same + /// preconditions (must use check-write first), but instead of + /// passing a list of bytes, you simply pass the number of zero-bytes + /// that should be written. + @since(version = 0.2.0) + write-zeroes: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Perform a write of up to 4096 zeroes, and then flush the stream. + /// Block until all of these operations are complete, or an error + /// occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write-zeroes`, and `flush`, and is implemented with + /// the following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while num_zeroes != 0 { + /// // Wait for the stream to become writable + /// pollable.block(); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, num_zeroes); + /// this.write-zeroes(len); // eliding error handling + /// num_zeroes -= len; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// pollable.block(); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + @since(version = 0.2.0) + blocking-write-zeroes-and-flush: func( + /// The number of zero-bytes to write + len: u64 + ) -> result<_, stream-error>; + + /// Read from one stream and write to another. + /// + /// The behavior of splice is equivalent to: + /// 1. calling `check-write` on the `output-stream` + /// 2. calling `read` on the `input-stream` with the smaller of the + /// `check-write` permitted length and the `len` provided to `splice` + /// 3. calling `write` on the `output-stream` with that read data. + /// + /// Any error reported by the call to `check-write`, `read`, or + /// `write` ends the splice and reports that error. + /// + /// This function returns the number of bytes transferred; it may be less + /// than `len`. + @since(version = 0.2.0) + splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + + /// Read from one stream and write to another, with blocking. + /// + /// This is similar to `splice`, except that it blocks until the + /// `output-stream` is ready for writing, and the `input-stream` + /// is ready for reading, before performing the `splice`. + @since(version = 0.2.0) + blocking-splice: func( + /// The stream to read from + src: borrow, + /// The number of bytes to splice + len: u64, + ) -> result; + } +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/io/world.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/io/world.wit new file mode 100644 index 00000000..84c85c08 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/random/insecure-seed.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/random/insecure-seed.wit new file mode 100644 index 00000000..d3dc03a6 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/random/insecure-seed.wit @@ -0,0 +1,27 @@ +package wasi:random@0.2.6; +/// The insecure-seed interface for seeding hash-map DoS resistance. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface insecure-seed { + /// Return a 128-bit value that may contain a pseudo-random value. + /// + /// The returned value is not required to be computed from a CSPRNG, and may + /// even be entirely deterministic. Host implementations are encouraged to + /// provide pseudo-random values to any program exposed to + /// attacker-controlled content, to enable DoS protection built into many + /// languages' hash-map implementations. + /// + /// This function is intended to only be called once, by a source language + /// to initialize Denial Of Service (DoS) protection in its hash-map + /// implementation. + /// + /// # Expected future evolution + /// + /// This will likely be changed to a value import, to prevent it from being + /// called multiple times and potentially used for purposes other than DoS + /// protection. + @since(version = 0.2.0) + insecure-seed: func() -> tuple; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/random/insecure.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/random/insecure.wit new file mode 100644 index 00000000..d4d02848 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/random/insecure.wit @@ -0,0 +1,25 @@ +package wasi:random@0.2.6; +/// The insecure interface for insecure pseudo-random numbers. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface insecure { + /// Return `len` insecure pseudo-random bytes. + /// + /// This function is not cryptographically secure. Do not use it for + /// anything related to security. + /// + /// There are no requirements on the values of the returned bytes, however + /// implementations are encouraged to return evenly distributed values with + /// a long period. + @since(version = 0.2.0) + get-insecure-random-bytes: func(len: u64) -> list; + + /// Return an insecure pseudo-random `u64` value. + /// + /// This function returns the same type of pseudo-random data as + /// `get-insecure-random-bytes`, represented as a `u64`. + @since(version = 0.2.0) + get-insecure-random-u64: func() -> u64; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/random/random.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/random/random.wit new file mode 100644 index 00000000..a0ff9564 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/random/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.2.6; +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.2.0) +interface random { + /// Return `len` cryptographically-secure random or pseudo-random bytes. + /// + /// This function must produce data at least as cryptographically secure and + /// fast as an adequately seeded cryptographically-secure pseudo-random + /// number generator (CSPRNG). It must not block, from the perspective of + /// the calling program, under any circumstances, including on the first + /// request and on requests for numbers of bytes. The returned data must + /// always be unpredictable. + /// + /// This function must always return fresh data. Deterministic environments + /// must omit this function, rather than implementing it with deterministic + /// data. + @since(version = 0.2.0) + get-random-bytes: func(len: u64) -> list; + + /// Return a cryptographically-secure random or pseudo-random `u64` value. + /// + /// This function returns the same type of data as `get-random-bytes`, + /// represented as a `u64`. + @since(version = 0.2.0) + get-random-u64: func() -> u64; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/random/world.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/random/world.wit new file mode 100644 index 00000000..099f47b3 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/random/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import random; + + @since(version = 0.2.0) + import insecure; + + @since(version = 0.2.0) + import insecure-seed; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/instance-network.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/instance-network.wit new file mode 100644 index 00000000..5f6e6c1c --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/instance-network.wit @@ -0,0 +1,11 @@ + +/// This interface provides a value-export of the default network handle.. +@since(version = 0.2.0) +interface instance-network { + @since(version = 0.2.0) + use network.{network}; + + /// Get a handle to the default network. + @since(version = 0.2.0) + instance-network: func() -> network; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/ip-name-lookup.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/ip-name-lookup.wit new file mode 100644 index 00000000..ee6419e7 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/ip-name-lookup.wit @@ -0,0 +1,56 @@ +@since(version = 0.2.0) +interface ip-name-lookup { + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-address}; + + /// Resolve an internet host name to a list of IP addresses. + /// + /// Unicode domain names are automatically converted to ASCII using IDNA encoding. + /// If the input is an IP address string, the address is parsed and returned + /// as-is without making any external requests. + /// + /// See the wasi-socket proposal README.md for a comparison with getaddrinfo. + /// + /// This function never blocks. It either immediately fails or immediately + /// returns successfully with a `resolve-address-stream` that can be used + /// to (asynchronously) fetch the results. + /// + /// # Typical errors + /// - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + resolve-addresses: func(network: borrow, name: string) -> result; + + @since(version = 0.2.0) + resource resolve-address-stream { + /// Returns the next address from the resolver. + /// + /// This function should be called multiple times. On each call, it will + /// return the next address in connection order preference. If all + /// addresses have been exhausted, this function returns `none`. + /// + /// This function never returns IPv4-mapped IPv6 addresses. + /// + /// # Typical errors + /// - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + /// - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + /// - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + /// - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + @since(version = 0.2.0) + resolve-next-address: func() -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/network.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/network.wit new file mode 100644 index 00000000..6ca98b63 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/network.wit @@ -0,0 +1,169 @@ +@since(version = 0.2.0) +interface network { + @unstable(feature = network-error-code) + use wasi:io/error@0.2.6.{error}; + + /// An opaque resource that represents access to (a subset of) the network. + /// This enables context-based security for networking. + /// There is no need for this to map 1:1 to a physical network interface. + @since(version = 0.2.0) + resource network; + + /// Error codes. + /// + /// In theory, every API can return any error code. + /// In practice, API's typically only return the errors documented per API + /// combined with a couple of errors that are always possible: + /// - `unknown` + /// - `access-denied` + /// - `not-supported` + /// - `out-of-memory` + /// - `concurrency-conflict` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.2.0) + enum error-code { + /// Unknown error + unknown, + + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + + /// The operation is not supported. + /// + /// POSIX equivalent: EOPNOTSUPP + not-supported, + + /// One of the arguments is invalid. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + + /// Not enough memory to complete the operation. + /// + /// POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + out-of-memory, + + /// The operation timed out before it could finish completely. + timeout, + + /// This operation is incompatible with another asynchronous operation that is already in progress. + /// + /// POSIX equivalent: EALREADY + concurrency-conflict, + + /// Trying to finish an asynchronous operation that: + /// - has not been started yet, or: + /// - was already finished by a previous `finish-*` call. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + not-in-progress, + + /// The operation has been aborted because it could not be completed immediately. + /// + /// Note: this is scheduled to be removed when `future`s are natively supported. + would-block, + + + /// The operation is not valid in the socket's current state. + invalid-state, + + /// A new socket resource could not be created because of a system limit. + new-socket-limit, + + /// A bind operation failed because the provided address is not an address that the `network` can bind to. + address-not-bindable, + + /// A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + address-in-use, + + /// The remote address is not reachable + remote-unreachable, + + + /// The TCP connection was forcefully rejected + connection-refused, + + /// The TCP connection was reset. + connection-reset, + + /// A TCP connection was aborted. + connection-aborted, + + + /// The size of a datagram sent to a UDP socket exceeded the maximum + /// supported size. + datagram-too-large, + + + /// Name does not exist or has no suitable associated IP addresses. + name-unresolvable, + + /// A temporary failure in name resolution occurred. + temporary-resolver-failure, + + /// A permanent failure in name resolution occurred. + permanent-resolver-failure, + } + + /// Attempts to extract a network-related `error-code` from the stream + /// `error` provided. + /// + /// Stream operations which return `stream-error::last-operation-failed` + /// have a payload with more information about the operation that failed. + /// This payload can be passed through to this function to see if there's + /// network-related information about the error to return. + /// + /// Note that this function is fallible because not all stream-related + /// errors are network-related errors. + @unstable(feature = network-error-code) + network-error-code: func(err: borrow) -> option; + + @since(version = 0.2.0) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.2.0) + type ipv4-address = tuple; + @since(version = 0.2.0) + type ipv6-address = tuple; + + @since(version = 0.2.0) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.2.0) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.2.0) + record ipv6-socket-address { + /// sin6_port + port: u16, + /// sin6_flowinfo + flow-info: u32, + /// sin6_addr + address: ipv6-address, + /// sin6_scope_id + scope-id: u32, + } + + @since(version = 0.2.0) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/tcp-create-socket.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/tcp-create-socket.wit new file mode 100644 index 00000000..eedbd307 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/tcp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface tcp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use tcp.{tcp-socket}; + + /// Create a new TCP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` + /// is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-tcp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/tcp.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/tcp.wit new file mode 100644 index 00000000..d7933614 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/tcp.wit @@ -0,0 +1,387 @@ +@since(version = 0.2.0) +interface tcp { + @since(version = 0.2.0) + use wasi:io/streams@0.2.6.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.6.{duration}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + @since(version = 0.2.0) + enum shutdown-type { + /// Similar to `SHUT_RD` in POSIX. + receive, + + /// Similar to `SHUT_WR` in POSIX. + send, + + /// Similar to `SHUT_RDWR` in POSIX. + both, + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bind-in-progress` + /// - `bound` (See note below) + /// - `listen-in-progress` + /// - `listening` + /// - `connect-in-progress` + /// - `connected` + /// - `closed` + /// See + /// for more information. + /// + /// Note: Except where explicitly mentioned, whenever this documentation uses + /// the term "bound" without backticks it actually means: in the `bound` state *or higher*. + /// (i.e. `bound`, `listen-in-progress`, `listening`, `connect-in-progress` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `network::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.2.0) + resource tcp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the TCP/UDP port is zero, the socket will be bound to a random free port. + /// + /// Bind can be attempted multiple times on the same socket, even with + /// different arguments on each iteration. But never concurrently and + /// only as long as the previous bind failed. Once a bind succeeds, the + /// binding can't be changed anymore. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + /// - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + /// state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + /// socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + /// and SO_REUSEADDR performs something different entirely. + /// + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Connect to a remote endpoint. + /// + /// On success: + /// - the socket is transitioned into the `connected` state. + /// - a pair of streams is returned that can be used to read & write to the connection + /// + /// After a failed connection attempt, the socket will be in the `closed` + /// state and the only valid action left is to `drop` the socket. A single + /// socket can not be used to connect more than once. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + /// - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + /// - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + /// - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + /// - `timeout`: Connection timed out. (ETIMEDOUT) + /// - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `connection-aborted`: The connection was aborted. (ECONNABORTED) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `not-in-progress`: A connect operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// The POSIX equivalent of `start-connect` is the regular `connect` syscall. + /// Because all WASI sockets are non-blocking this is expected to return + /// EINPROGRESS, which should be translated to `ok()` in WASI. + /// + /// The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` + /// with a timeout of 0 on the socket descriptor. Followed by a check for + /// the `SO_ERROR` socket option, in case the poll signaled readiness. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-connect: func(network: borrow, remote-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-connect: func() -> result, error-code>; + + /// Start listening for new connections. + /// + /// Transitions the socket into the `listening` state. + /// + /// Unlike POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + /// - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + /// - `invalid-state`: The socket is already in the `listening` state. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + /// - `not-in-progress`: A listen operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the listen operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `listen` as part of either `start-listen` or `finish-listen`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-listen: func() -> result<_, error-code>; + @since(version = 0.2.0) + finish-listen: func() -> result<_, error-code>; + + /// Accept a new client socket. + /// + /// The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: + /// - `address-family` + /// - `keep-alive-enabled` + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// - `hop-limit` + /// - `receive-buffer-size` + /// - `send-buffer-size` + /// + /// On success, this function returns the newly accepted client socket along with + /// a pair of streams that can be used to read & write to the connection. + /// + /// # Typical errors + /// - `invalid-state`: Socket is not in the `listening` state. (EINVAL) + /// - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + /// - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + accept: func() -> result, error-code>; + + /// Get the bound local address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the remote address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.2.0) + is-listening: func() -> bool; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Hints the desired listen queue size. Implementations are free to ignore this. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// + /// # Typical errors + /// - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + /// - `invalid-argument`: (set) The provided value was 0. + /// - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + @since(version = 0.2.0) + set-listen-backlog-size: func(value: u64) -> result<_, error-code>; + + /// Enables or disables keepalive. + /// + /// The keepalive behavior can be adjusted using: + /// - `keep-alive-idle-time` + /// - `keep-alive-interval` + /// - `keep-alive-count` + /// These properties can be configured while `keep-alive-enabled` is false, but only come into effect when `keep-alive-enabled` is true. + /// + /// Equivalent to the SO_KEEPALIVE socket option. + @since(version = 0.2.0) + keep-alive-enabled: func() -> result; + @since(version = 0.2.0) + set-keep-alive-enabled: func(value: bool) -> result<_, error-code>; + + /// Amount of time the connection has to be idle before TCP starts sending keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPIDLE socket option. (TCP_KEEPALIVE on MacOS) + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-idle-time: func() -> result; + @since(version = 0.2.0) + set-keep-alive-idle-time: func(value: duration) -> result<_, error-code>; + + /// The time between keepalive packets. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPINTVL socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-interval: func() -> result; + @since(version = 0.2.0) + set-keep-alive-interval: func(value: duration) -> result<_, error-code>; + + /// The maximum amount of keepalive packets TCP should send before aborting the connection. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the TCP_KEEPCNT socket option. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + keep-alive-count: func() -> result; + @since(version = 0.2.0) + set-keep-alive-count: func(value: u32) -> result<_, error-code>; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + hop-limit: func() -> result; + @since(version = 0.2.0) + set-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which can be used to poll for, or block on, + /// completion of any of the asynchronous operations of this socket. + /// + /// When `finish-bind`, `finish-listen`, `finish-connect` or `accept` + /// return `error(would-block)`, this pollable can be used to wait for + /// their success or failure, after which the method can be retried. + /// + /// The pollable is not limited to the async operation that happens to be + /// in progress at the time of calling `subscribe` (if any). Theoretically, + /// `subscribe` only has to be called once per socket and can then be + /// (re)used for the remainder of the socket's lifetime. + /// + /// See + /// for more information. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + + /// Initiate a graceful shutdown. + /// + /// - `receive`: The socket is not expecting to receive any data from + /// the peer. The `input-stream` associated with this socket will be + /// closed. Any data still in the receive queue at time of calling + /// this method will be discarded. + /// - `send`: The socket has no more data to send to the peer. The `output-stream` + /// associated with this socket will be closed and a FIN packet will be sent. + /// - `both`: Same effect as `receive` & `send` combined. + /// + /// This function is idempotent; shutting down a direction more than once + /// has no effect and returns `ok`. + /// + /// The shutdown function does not close (drop) the socket. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + shutdown: func(shutdown-type: shutdown-type) -> result<_, error-code>; + } +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/udp-create-socket.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/udp-create-socket.wit new file mode 100644 index 00000000..e8eeacbf --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/udp-create-socket.wit @@ -0,0 +1,30 @@ +@since(version = 0.2.0) +interface udp-create-socket { + @since(version = 0.2.0) + use network.{network, error-code, ip-address-family}; + @since(version = 0.2.0) + use udp.{udp-socket}; + + /// Create a new UDP socket. + /// + /// Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + /// On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + /// + /// This function does not require a network capability handle. This is considered to be safe because + /// at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, + /// the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + /// + /// All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + /// + /// # Typical errors + /// - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + /// - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + create-udp-socket: func(address-family: ip-address-family) -> result; +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/udp.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/udp.wit new file mode 100644 index 00000000..af52f831 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/udp.wit @@ -0,0 +1,288 @@ +@since(version = 0.2.0) +interface udp { + @since(version = 0.2.0) + use wasi:io/poll@0.2.6.{pollable}; + @since(version = 0.2.0) + use network.{network, error-code, ip-socket-address, ip-address-family}; + + /// A received datagram. + @since(version = 0.2.0) + record incoming-datagram { + /// The payload. + /// + /// Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + data: list, + + /// The source address. + /// + /// This field is guaranteed to match the remote address the stream was initialized with, if any. + /// + /// Equivalent to the `src_addr` out parameter of `recvfrom`. + remote-address: ip-socket-address, + } + + /// A datagram to be sent out. + @since(version = 0.2.0) + record outgoing-datagram { + /// The payload. + data: list, + + /// The destination address. + /// + /// The requirements on this field depend on how the stream was initialized: + /// - with a remote address: this field must be None or match the stream's remote address exactly. + /// - without a remote address: this field is required. + /// + /// If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. + remote-address: option, + } + + /// A UDP socket handle. + @since(version = 0.2.0) + resource udp-socket { + /// Bind the socket to a specific network on the provided IP address and port. + /// + /// If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + /// network interface(s) to bind to. + /// If the port is zero, the socket will be bound to a random free port. + /// + /// # Typical errors + /// - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + /// - `invalid-state`: The socket is already bound. (EINVAL) + /// - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + /// - `address-in-use`: Address is already in use. (EADDRINUSE) + /// - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + /// - `not-in-progress`: A `bind` operation is not in progress. + /// - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + /// + /// # Implementors note + /// Unlike in POSIX, in WASI the bind operation is async. This enables + /// interactive WASI hosts to inject permission prompts. Runtimes that + /// don't want to make use of this ability can simply call the native + /// `bind` as part of either `start-bind` or `finish-bind`. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + start-bind: func(network: borrow, local-address: ip-socket-address) -> result<_, error-code>; + @since(version = 0.2.0) + finish-bind: func() -> result<_, error-code>; + + /// Set up inbound & outbound communication channels, optionally to a specific peer. + /// + /// This function only changes the local socket configuration and does not generate any network traffic. + /// On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, + /// based on the best network path to `remote-address`. + /// + /// When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// This method may be called multiple times on the same socket to change its association, but + /// only the most recently returned pair of streams will be operational. Implementations may trap if + /// the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. + /// + /// The POSIX equivalent in pseudo-code is: + /// ```text + /// if (was previously connected) { + /// connect(s, AF_UNSPEC) + /// } + /// if (remote_address is Some) { + /// connect(s, remote_address) + /// } + /// ``` + /// + /// Unlike in POSIX, the socket must already be explicitly bound. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-state`: The socket is not bound. + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + %stream: func(remote-address: option) -> result, error-code>; + + /// Get the current bound address. + /// + /// POSIX mentions: + /// > If the socket has not been bound to a local name, the value + /// > stored in the object pointed to by `address` is unspecified. + /// + /// WASI is stricter and requires `local-address` to return `invalid-state` when the socket hasn't been bound yet. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not bound to any local address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + local-address: func() -> result; + + /// Get the address the socket is currently streaming to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + remote-address: func() -> result; + + /// Whether this is a IPv4 or IPv6 socket. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.2.0) + address-family: func() -> ip-address-family; + + /// Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The TTL value must be 1 or higher. + @since(version = 0.2.0) + unicast-hop-limit: func() -> result; + @since(version = 0.2.0) + set-unicast-hop-limit: func(value: u8) -> result<_, error-code>; + + /// The kernel buffer space reserved for sends/receives on this socket. + /// + /// If the provided value is 0, an `invalid-argument` error is returned. + /// Any other value will never cause an error, but it might be silently clamped and/or rounded. + /// I.e. after setting a value, reading the same setting back may return a different value. + /// + /// Equivalent to the SO_RCVBUF and SO_SNDBUF socket options. + /// + /// # Typical errors + /// - `invalid-argument`: (set) The provided value was 0. + @since(version = 0.2.0) + receive-buffer-size: func() -> result; + @since(version = 0.2.0) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.2.0) + send-buffer-size: func() -> result; + @since(version = 0.2.0) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + + /// Create a `pollable` which will resolve once the socket is ready for I/O. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource incoming-datagram-stream { + /// Receive messages on the socket. + /// + /// This function attempts to receive up to `max-results` datagrams on the socket without blocking. + /// The returned list may contain fewer elements than requested, but never more. + /// + /// This function returns successfully with an empty list when either: + /// - `max-results` is 0, or: + /// - `max-results` is greater than 0, but no results are immediately available. + /// This function never returns `error(would-block)`. + /// + /// # Typical errors + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + receive: func(max-results: u64) -> result, error-code>; + + /// Create a `pollable` which will resolve once the stream is ready to receive again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } + + @since(version = 0.2.0) + resource outgoing-datagram-stream { + /// Check readiness for sending. This function never blocks. + /// + /// Returns the number of datagrams permitted for the next call to `send`, + /// or an error. Calling `send` with more datagrams than this function has + /// permitted will trap. + /// + /// When this function returns ok(0), the `subscribe` pollable will + /// become ready when this function will report at least ok(1), or an + /// error. + /// + /// Never returns `would-block`. + check-send: func() -> result; + + /// Send messages on the socket. + /// + /// This function attempts to send all provided `datagrams` on the socket without blocking and + /// returns how many messages were actually sent (or queued for sending). This function never + /// returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. + /// + /// This function semantically behaves the same as iterating the `datagrams` list and sequentially + /// sending each individual datagram until either the end of the list has been reached or the first error occurred. + /// If at least one datagram has been sent successfully, this function never returns an error. + /// + /// If the input list is empty, the function returns `ok(0)`. + /// + /// Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if + /// either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. + /// + /// # Typical errors + /// - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + /// - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + /// - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) + /// - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + /// - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// - `connection-refused`: The connection was refused. (ECONNREFUSED) + /// - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.2.0) + send: func(datagrams: list) -> result; + + /// Create a `pollable` which will resolve once the stream is ready to send again. + /// + /// Note: this function is here for WASI 0.2 only. + /// It's planned to be removed when `future` is natively supported in Preview3. + @since(version = 0.2.0) + subscribe: func() -> pollable; + } +} diff --git a/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/world.wit b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/world.wit new file mode 100644 index 00000000..e86f02ce --- /dev/null +++ b/crates/wasm-runtime-guest/wit/deps-0.2.6/sockets/world.wit @@ -0,0 +1,19 @@ +package wasi:sockets@0.2.6; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import instance-network; + @since(version = 0.2.0) + import network; + @since(version = 0.2.0) + import udp; + @since(version = 0.2.0) + import udp-create-socket; + @since(version = 0.2.0) + import tcp; + @since(version = 0.2.0) + import tcp-create-socket; + @since(version = 0.2.0) + import ip-name-lookup; +} diff --git a/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/atomic.wit b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/atomic.wit new file mode 100644 index 00000000..1e8106ec --- /dev/null +++ b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/atomic.wit @@ -0,0 +1,22 @@ +/// A keyvalue interface that provides atomic operations. +/// +/// Atomic operations are single, indivisible operations. When a fault causes an atomic operation to +/// fail, it will appear to the invoker of the atomic operation that the action either completed +/// successfully or did nothing at all. +/// +/// Please note that this interface is bare functions that take a reference to a bucket. This is to +/// get around the current lack of a way to "extend" a resource with additional methods inside of +/// wit. Future version of the interface will instead extend these methods on the base `bucket` +/// resource. +interface atomics { + use store.{bucket, error}; + + /// Atomically increment the value associated with the key in the store by the given delta. It + /// returns the new value. + /// + /// If the key does not exist in the store, it creates a new key-value pair with the value set + /// to the given delta. + /// + /// If any other error occurs, it returns an `Err(error)`. + increment: func(bucket: borrow, key: string, delta: u64) -> result; +} diff --git a/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/batch.wit b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/batch.wit new file mode 100644 index 00000000..c642dd3f --- /dev/null +++ b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/batch.wit @@ -0,0 +1,63 @@ +/// A keyvalue interface that provides batch operations. +/// +/// A batch operation is an operation that operates on multiple keys at once. +/// +/// Batch operations are useful for reducing network round-trip time. For example, if you want to +/// get the values associated with 100 keys, you can either do 100 get operations or you can do 1 +/// batch get operation. The batch operation is faster because it only needs to make 1 network call +/// instead of 100. +/// +/// A batch operation does not guarantee atomicity, meaning that if the batch operation fails, some +/// of the keys may have been modified and some may not. +/// +/// This interface does has the same consistency guarantees as the `store` interface, meaning that +/// you should be able to "read your writes." +/// +/// Please note that this interface is bare functions that take a reference to a bucket. This is to +/// get around the current lack of a way to "extend" a resource with additional methods inside of +/// wit. Future version of the interface will instead extend these methods on the base `bucket` +/// resource. +interface batch { + use store.{bucket, error}; + + /// Get the key-value pairs associated with the keys in the store. It returns a list of + /// key-value pairs. + /// + /// If any of the keys do not exist in the store, it returns a `none` value for that pair in the + /// list. + /// + /// MAY show an out-of-date value if there are concurrent writes to the store. + /// + /// If any other error occurs, it returns an `Err(error)`. + get-many: func(bucket: borrow, keys: list) -> result>>>, error>; + + /// Set the values associated with the keys in the store. If the key already exists in the + /// store, it overwrites the value. + /// + /// Note that the key-value pairs are not guaranteed to be set in the order they are provided. + /// + /// If any of the keys do not exist in the store, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not + /// rollback the key-value pairs that were already set. Thus, this batch operation does not + /// guarantee atomicity, implying that some key-value pairs could be set while others might + /// fail. + /// + /// Other concurrent operations may also be able to see the partial results. + set-many: func(bucket: borrow, key-values: list>>) -> result<_, error>; + + /// Delete the key-value pairs associated with the keys in the store. + /// + /// Note that the key-value pairs are not guaranteed to be deleted in the order they are + /// provided. + /// + /// If any of the keys do not exist in the store, it skips the key. + /// + /// If any other error occurs, it returns an `Err(error)`. When an error occurs, it does not + /// rollback the key-value pairs that were already deleted. Thus, this batch operation does not + /// guarantee atomicity, implying that some key-value pairs could be deleted while others might + /// fail. + /// + /// Other concurrent operations may also be able to see the partial results. + delete-many: func(bucket: borrow, keys: list) -> result<_, error>; +} diff --git a/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/store.wit b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/store.wit new file mode 100644 index 00000000..91273536 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/store.wit @@ -0,0 +1,122 @@ +/// A keyvalue interface that provides eventually consistent key-value operations. +/// +/// Each of these operations acts on a single key-value pair. +/// +/// The value in the key-value pair is defined as a `u8` byte array and the intention is that it is +/// the common denominator for all data types defined by different key-value stores to handle data, +/// ensuring compatibility between different key-value stores. Note: the clients will be expecting +/// serialization/deserialization overhead to be handled by the key-value store. The value could be +/// a serialized object from JSON, HTML or vendor-specific data types like AWS S3 objects. +/// +/// Data consistency in a key value store refers to the guarantee that once a write operation +/// completes, all subsequent read operations will return the value that was written. +/// +/// Any implementation of this interface must have enough consistency to guarantee "reading your +/// writes." In particular, this means that the client should never get a value that is older than +/// the one it wrote, but it MAY get a newer value if one was written around the same time. These +/// guarantees only apply to the same client (which will likely be provided by the host or an +/// external capability of some kind). In this context a "client" is referring to the caller or +/// guest that is consuming this interface. Once a write request is committed by a specific client, +/// all subsequent read requests by the same client will reflect that write or any subsequent +/// writes. Another client running in a different context may or may not immediately see the result +/// due to the replication lag. As an example of all of this, if a value at a given key is A, and +/// the client writes B, then immediately reads, it should get B. If something else writes C in +/// quick succession, then the client may get C. However, a client running in a separate context may +/// still see A or B +interface store { + /// The set of errors which may be raised by functions in this package + variant error { + /// The host does not recognize the store identifier requested. + no-such-store, + + /// The requesting component does not have access to the specified store + /// (which may or may not exist). + access-denied, + + /// Some implementation-specific error has occurred (e.g. I/O) + other(string) + } + + /// A response to a `list-keys` operation. + record key-response { + /// The list of keys returned by the query. + keys: list, + /// The continuation token to use to fetch the next page of keys. If this is `null`, then + /// there are no more keys to fetch. + cursor: option + } + + /// Get the bucket with the specified identifier. + /// + /// `identifier` must refer to a bucket provided by the host. + /// + /// `error::no-such-store` will be raised if the `identifier` is not recognized. + open: func(identifier: string) -> result; + + /// A bucket is a collection of key-value pairs. Each key-value pair is stored as a entry in the + /// bucket, and the bucket itself acts as a collection of all these entries. + /// + /// It is worth noting that the exact terminology for bucket in key-value stores can very + /// depending on the specific implementation. For example: + /// + /// 1. Amazon DynamoDB calls a collection of key-value pairs a table + /// 2. Redis has hashes, sets, and sorted sets as different types of collections + /// 3. Cassandra calls a collection of key-value pairs a column family + /// 4. MongoDB calls a collection of key-value pairs a collection + /// 5. Riak calls a collection of key-value pairs a bucket + /// 6. Memcached calls a collection of key-value pairs a slab + /// 7. Azure Cosmos DB calls a collection of key-value pairs a container + /// + /// In this interface, we use the term `bucket` to refer to a collection of key-value pairs + resource bucket { + /// Get the value associated with the specified `key` + /// + /// The value is returned as an option. If the key-value pair exists in the + /// store, it returns `Ok(value)`. If the key does not exist in the + /// store, it returns `Ok(none)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + get: func(key: string) -> result>, error>; + + /// Set the value associated with the key in the store. If the key already + /// exists in the store, it overwrites the value. + /// + /// If the key does not exist in the store, it creates a new key-value pair. + /// + /// If any other error occurs, it returns an `Err(error)`. + set: func(key: string, value: list) -> result<_, error>; + + /// Delete the key-value pair associated with the key in the store. + /// + /// If the key does not exist in the store, it does nothing. + /// + /// If any other error occurs, it returns an `Err(error)`. + delete: func(key: string) -> result<_, error>; + + /// Check if the key exists in the store. + /// + /// If the key exists in the store, it returns `Ok(true)`. If the key does + /// not exist in the store, it returns `Ok(false)`. + /// + /// If any other error occurs, it returns an `Err(error)`. + exists: func(key: string) -> result; + + /// Get all the keys in the store with an optional cursor (for use in pagination). It + /// returns a list of keys. Please note that for most KeyValue implementations, this is a + /// can be a very expensive operation and so it should be used judiciously. Implementations + /// can return any number of keys in a single response, but they should never attempt to + /// send more data than is reasonable (i.e. on a small edge device, this may only be a few + /// KB, while on a large machine this could be several MB). Any response should also return + /// a cursor that can be used to fetch the next page of keys. See the `key-response` record + /// for more information. + /// + /// Note that the keys are not guaranteed to be returned in any particular order. + /// + /// If the store is empty, it returns an empty list. + /// + /// MAY show an out-of-date list of keys if there are concurrent writes to the store. + /// + /// If any error occurs, it returns an `Err(error)`. + list-keys: func(cursor: option) -> result; + } +} diff --git a/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/watch.wit b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/watch.wit new file mode 100644 index 00000000..4c77960c --- /dev/null +++ b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/watch.wit @@ -0,0 +1,16 @@ +/// A keyvalue interface that provides watch operations. +/// +/// This interface is used to provide event-driven mechanisms to handle +/// keyvalue changes. +interface watcher { + /// A keyvalue interface that provides handle-watch operations. + use store.{bucket}; + + /// Handle the `set` event for the given bucket and key. It includes a reference to the `bucket` + /// that can be used to interact with the store. + on-set: func(bucket: bucket, key: string, value: list); + + /// Handle the `delete` event for the given bucket and key. It includes a reference to the + /// `bucket` that can be used to interact with the store. + on-delete: func(bucket: bucket, key: string); +} diff --git a/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/world.wit b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/world.wit new file mode 100644 index 00000000..8dd33046 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/keyvalue-0.2.0-draft/world.wit @@ -0,0 +1,26 @@ +package wasi:keyvalue@0.2.0-draft; + +/// The `wasi:keyvalue/imports` world provides common APIs for interacting with key-value stores. +/// Components targeting this world will be able to do: +/// +/// 1. CRUD (create, read, update, delete) operations on key-value stores. +/// 2. Atomic `increment` and CAS (compare-and-swap) operations. +/// 3. Batch operations that can reduce the number of round trips to the network. +world imports { + /// The `store` capability allows the component to perform eventually consistent operations on + /// the key-value store. + import store; + + /// The `atomic` capability allows the component to perform atomic / `increment` and CAS + /// (compare-and-swap) operations. + import atomics; + + /// The `batch` capability allows the component to perform eventually consistent batch + /// operations that can reduce the number of round trips to the network. + import batch; +} + +world watch-service { + include imports; + export watcher; +} diff --git a/crates/wasm-runtime-guest/wit/trailbase.wit b/crates/wasm-runtime-guest/wit/trailbase.wit new file mode 100644 index 00000000..f98858d9 --- /dev/null +++ b/crates/wasm-runtime-guest/wit/trailbase.wit @@ -0,0 +1,78 @@ +package trailbase:runtime; + +interface init-endpoint { + enum method-type { + get, + post, + head, + options, + patch, + delete, + put, + trace, + connect, + } + + record init-result { + /// Registered http handlers (method, path)[]. + http-handlers: list>, + + /// Registered jobs (name, spec)[]. + job-handlers: list>, + } + + init: func() -> init-result; +} + +interface host-endpoint { + thread-id: func() -> u64; + + + variant tx-error { + other(string) + } + + variant value { + null, + text(string), + blob(list), + integer(s64), + real(f64), + } + + // NOTE: Ideally, we'd use these but they currently block guests. + execute: func(query: string, params: list) -> result; + query: func(query: string, params: list) -> result>, tx-error>; + + // However, transactions have to be sync. + tx-begin: func() -> result<_, tx-error>; + tx-commit: func() -> result<_, tx-error>; + tx-rollback: func() -> result<_, tx-error>; + + tx-execute: func(query: string, params: list) -> result; + tx-query: func(query: string, params: list) -> result>, tx-error>; +} + +// Note: +// * imports are provided by the host +// * exports are provided by the guest +// * includes to include a world into another world. +world trailbase { + // Pull in WASIp2 http interface for outbound requests. + import wasi:http/outgoing-handler@0.2.6; + + // Pull in WASIp2 filesystem interfaces. + include wasi:filesystem/imports@0.2.6; + + // Pull in WASI random interfaces. + include wasi:random/imports@0.2.6; + + // Pull in WSAI Key-Value interfaces. + include wasi:keyvalue/imports@0.2.0-draft; + + // Host-provided interfaces. + import host-endpoint; + + // Guest-provided interfaces. + export init-endpoint; +} diff --git a/crates/wasm-runtime-host/Cargo.toml b/crates/wasm-runtime-host/Cargo.toml new file mode 100644 index 00000000..2157c4dd --- /dev/null +++ b/crates/wasm-runtime-host/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "trailbase-wasm-runtime-host" +version = "0.1.0" +edition = "2024" +license = "OSL-3.0" +description = "WASM runtime for the TrailBase framework" +homepage = "https://trailbase.io" +exclude = [ + "**/node_modules/", + "**/dist/", +] + +[dependencies] +bytes = "1.10.1" +futures-util = "0.3.31" +http = "1.3.1" +http-body-util = "0.1.3" +hyper = "1.6.0" +kanal = "0.1.1" +log = { version = "^0.4.21", default-features = false } +parking_lot = { workspace = true } +rusqlite = { workspace = true } +self_cell = "1.2.0" +serde = { version = "^1.0.203", features = ["derive"] } +serde_json = "^1.0.117" +thiserror = "2.0.14" +trailbase-schema = { workspace = true } +trailbase-sqlite = { workspace = true } +trailbase-wasm-common = { workspace = true } +trailbase-wasi-keyvalue = { workspace = true } +tokio = { version = "^1.38.0", features = ["macros", "rt-multi-thread"] } +tracing = "0.1.41" +wasmtime = { workspace = true } +wasmtime-wasi = { workspace = true } +wasmtime-wasi-http = { workspace = true } +wasmtime-wasi-io = { workspace = true } diff --git a/crates/wasm-runtime-host/src/lib.rs b/crates/wasm-runtime-host/src/lib.rs new file mode 100644 index 00000000..00593b65 --- /dev/null +++ b/crates/wasm-runtime-host/src/lib.rs @@ -0,0 +1,896 @@ +#![forbid(clippy::unwrap_used)] +#![allow(clippy::needless_return)] +#![warn(clippy::await_holding_lock, clippy::inefficient_to_string)] + +mod sqlite; + +use bytes::Bytes; +use core::future::Future; +use futures_util::TryFutureExt; +use futures_util::future::LocalBoxFuture; +use http_body_util::combinators::BoxBody; +use parking_lot::Mutex; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::time::SystemTime; +use trailbase::runtime::host_endpoint::{TxError, Value}; +use trailbase_sqlite::{Params, Rows}; +use trailbase_wasi_keyvalue::WasiKeyValueCtx; +use wasmtime::component::{Component, HasSelf, Linker, ResourceTable}; +use wasmtime::{Config, Engine, Result, Store}; +use wasmtime_wasi::p2::add_to_linker_async; +use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxView}; +use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView}; +use wasmtime_wasi_http::bindings::http::types::ErrorCode; +use wasmtime_wasi_http::{WasiHttpCtx, WasiHttpView}; +use wasmtime_wasi_io::IoView; + +use crate::exports::trailbase::runtime::init_endpoint::InitResult; + +pub use trailbase_wasi_keyvalue::Store as KvStore; + +static IN_FLIGHT: AtomicUsize = AtomicUsize::new(0); + +// Documentation: https://docs.wasmtime.dev/api/wasmtime/component/macro.bindgen.html +wasmtime::component::bindgen!({ + world: "trailbase:runtime/trailbase", + path: [ + // Order-sensitive: will import *.wit from the folder. + "wit/deps-0.2.6/random", + "wit/deps-0.2.6/io", + "wit/deps-0.2.6/clocks", + "wit/deps-0.2.6/filesystem", + "wit/deps-0.2.6/sockets", + "wit/deps-0.2.6/cli", + "wit/deps-0.2.6/http", + "wit/keyvalue-0.2.0-draft", + // Ours: + "wit/trailbase.wit", + ], + // NOTE: This doesn't seem to work even though it should be fixed: + // https://github.com/bytecodealliance/wasmtime/issues/10677 + // i.e. can't add db locks to shared state. + require_store_data_send: false, + // NOTE: Doesn't work: https://github.com/bytecodealliance/wit-bindgen/issues/812. + // additional_derives: [ + // serde::Deserialize, + // serde::Serialize, + // ], + // Interactions with `ResourceTable` can possibly trap so enable the ability + // to return traps from generated functions. + imports: { + "trailbase:runtime/host-endpoint/tx-commit": trappable, + "trailbase:runtime/host-endpoint/tx-rollback": trappable, + "trailbase:runtime/host-endpoint/tx-execute": trappable, + "trailbase:runtime/host-endpoint/tx-query": trappable, + "trailbase:runtime/host-endpoint/thread-id": trappable, + default: async | trappable, + }, + exports: { + default: async, + }, +}); + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Wasmtime: {0}")] + Wasmtime(#[from] wasmtime::Error), + #[error("Channel closed")] + ChannelClosed, + #[error("Http Error: {0}")] + HttpErrorCode(ErrorCode), + #[error("Encoding")] + Encoding, + #[error("Other: {0}")] + Other(String), +} + +pub enum Message { + Run(Box) -> LocalBoxFuture<'static, ()> + Send>), +} + +#[derive(Clone)] +struct LockedTransaction(Rc>>); + +unsafe impl Send for LockedTransaction {} + +struct State { + resource_table: ResourceTable, + wasi_ctx: WasiCtx, + http: WasiHttpCtx, + kv: WasiKeyValueCtx, + + shared: Arc, + tx: LockedTransaction, +} + +impl Drop for State { + fn drop(&mut self) { + #[cfg(debug_assertions)] + if self.tx.0.lock().is_some() { + log::warn!("pending transaction locking the DB"); + } + } +} + +impl IoView for State { + fn table(&mut self) -> &mut ResourceTable { + return &mut self.resource_table; + } +} + +impl WasiView for State { + fn ctx(&mut self) -> WasiCtxView<'_> { + return WasiCtxView { + ctx: &mut self.wasi_ctx, + table: &mut self.resource_table, + }; + } +} + +impl WasiHttpView for State { + fn ctx(&mut self) -> &mut WasiHttpCtx { + return &mut self.http; + } + + fn table(&mut self) -> &mut ResourceTable { + return &mut self.resource_table; + } + + /// Receives HTTP fetches from the guest. + /// + /// Based on `WasiView`' default implementation. + fn send_request( + &mut self, + request: hyper::Request, + config: wasmtime_wasi_http::types::OutgoingRequestConfig, + ) -> wasmtime_wasi_http::HttpResult { + // log::debug!( + // "send_request {:?} {}: {request:?}", + // request.uri().host(), + // request.uri().path() + // ); + + return match request.uri().host() { + Some("__sqlite") => { + let conn = self.shared.conn.clone(); + Ok( + wasmtime_wasi_http::types::HostFutureIncomingResponse::pending( + wasmtime_wasi::runtime::spawn(async move { + Ok(sqlite::handle_sqlite_request(conn, request).await) + }), + ), + ) + } + _ => { + let handle = wasmtime_wasi::runtime::spawn(async move { + Ok(wasmtime_wasi_http::types::default_send_request_handler(request, config).await) + }); + Ok(wasmtime_wasi_http::types::HostFutureIncomingResponse::pending(handle)) + } + }; + } +} + +impl trailbase::runtime::host_endpoint::Host for State { + fn thread_id(&mut self) -> wasmtime::Result { + return Ok(self.shared.thread_id); + } + + fn execute( + &mut self, + query: String, + params: Vec, + ) -> impl Future>> + Send { + let conn = self.shared.conn.clone(); + let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect(); + + return self + .shared + .runtime + .spawn(async move { + conn + .execute(query, params) + .await + .map_err(|err| TxError::Other(err.to_string())) + .map(|v| v as u64) + }) + .map_err(|err| wasmtime::Error::msg(err.to_string())); + } + + fn query( + &mut self, + query: String, + params: Vec, + ) -> impl Future>, TxError>>> + Send { + let conn = self.shared.conn.clone(); + let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect(); + + return self + .shared + .runtime + .spawn(async move { + let rows = conn + .write_query_rows(query, params) + .await + .map_err(|err| TxError::Other(err.to_string()))?; + + let values: Vec<_> = rows + .into_iter() + .map(|trailbase_sqlite::Row(row, _col)| { + return row.into_iter().map(from_sqlite_value).collect::>(); + }) + .collect(); + + Ok(values) + }) + .map_err(|err| wasmtime::Error::msg(err.to_string())); + } + + fn tx_begin(&mut self) -> impl Future>> + Send { + async fn begin( + conn: trailbase_sqlite::Connection, + tx: LockedTransaction, + ) -> Result<(), TxError> { + assert!(tx.0.lock().is_none()); + + *tx.0.lock() = Some( + sqlite::new_tx(conn) + .await + .map_err(|err| TxError::Other(err.to_string()))?, + ); + + return Ok(()); + } + + let tx = self.tx.clone(); + return self + .shared + .runtime + .spawn(begin(self.shared.conn.clone(), tx)) + .map_err(|err| wasmtime::Error::msg(err.to_string())); + } + + fn tx_commit(&mut self) -> wasmtime::Result> { + fn commit(tx: LockedTransaction) -> Result<(), TxError> { + let Some(tx) = tx.0.lock().take() else { + return Err(TxError::Other("no pending tx".to_string())); + }; + + // NOTE: this is the same as `tx.commit()` just w/o consuming. + let lock = tx.borrow_dependent(); + lock + .execute_batch("COMMIT") + .map_err(|err| TxError::Other(err.to_string()))?; + + return Ok(()); + } + + return Ok(commit(self.tx.clone())); + } + + fn tx_rollback(&mut self) -> wasmtime::Result> { + fn rollback(tx: LockedTransaction) -> Result<(), TxError> { + let Some(tx) = tx.0.lock().take() else { + return Err(TxError::Other("no pending tx".to_string())); + }; + + // NOTE: this is the same as `tx.rollback()` just w/o consuming. + let lock = tx.borrow_dependent(); + lock + .execute_batch("ROLLBACK") + .map_err(|err| TxError::Other(err.to_string()))?; + + return Ok(()); + } + + return Ok(rollback(self.tx.clone())); + } + + fn tx_execute( + &mut self, + query: String, + params: Vec, + ) -> wasmtime::Result> { + fn execute(tx: LockedTransaction, query: String, params: Vec) -> Result { + let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect(); + + let Some(ref tx) = *tx.0.lock() else { + return Err(TxError::Other("No open transaction".to_string())); + }; + + let lock = tx.borrow_dependent(); + let mut stmt = lock + .prepare(&query) + .map_err(|err| TxError::Other(err.to_string()))?; + + params + .bind(&mut stmt) + .map_err(|err| TxError::Other(err.to_string()))?; + + return Ok( + stmt + .raw_execute() + .map_err(|err| TxError::Other(err.to_string()))? as u64, + ); + } + + return Ok(execute(self.tx.clone(), query, params)); + } + + fn tx_query( + &mut self, + query: String, + params: Vec, + ) -> wasmtime::Result>, TxError>> { + fn query_fn( + tx: LockedTransaction, + query: String, + params: Vec, + ) -> Result>, TxError> { + let params: Vec<_> = params.into_iter().map(to_sqlite_value).collect(); + + let Some(ref tx) = *tx.0.lock() else { + return Err(TxError::Other("No open transaction".to_string())); + }; + + let lock = tx.borrow_dependent(); + let mut stmt = lock + .prepare(&query) + .map_err(|err| TxError::Other(err.to_string()))?; + + params + .bind(&mut stmt) + .map_err(|err| TxError::Other(err.to_string()))?; + + let rows = + Rows::from_rows(stmt.raw_query()).map_err(|err| TxError::Other(err.to_string()))?; + + let values: Vec<_> = rows + .into_iter() + .map(|trailbase_sqlite::Row(row, _col)| { + return row.into_iter().map(from_sqlite_value).collect::>(); + }) + .collect(); + + return Ok(values); + } + + return Ok(query_fn(self.tx.clone(), query, params)); + } +} + +pub struct Runtime { + // Shared sender. + shared_sender: kanal::AsyncSender, + threads: Vec<(std::thread::JoinHandle<()>, kanal::AsyncSender)>, +} + +impl Drop for Runtime { + fn drop(&mut self) { + for (handle, ch) in std::mem::take(&mut self.threads) { + // Dropping the private channel will trigger the event_loop to return. + drop(ch); + + if let Err(err) = handle.join() { + log::error!("Failed to join main rt thread: {err:?}"); + } + } + } +} + +fn build_config(cache: Option) -> Config { + let mut config = Config::new(); + + // Execution settings. + config.async_support(true); + config.epoch_interruption(false); + config.memory_reservation(64 * 1024 * 1024 /* bytes */); + // config.wasm_backtrace_details(wasmtime::WasmBacktraceDetails::Enable); + + // Compilation settings. + config.cache(cache); + config.cranelift_opt_level(wasmtime::OptLevel::Speed); + config.parallel_compilation(true); + + return config; +} + +impl Runtime { + pub fn new( + n_threads: usize, + wasm_source_file: std::path::PathBuf, + conn: trailbase_sqlite::Connection, + kv_store: KvStore, + fs_root_path: Option, + ) -> Result { + let engine = Engine::new(&build_config(Some(wasmtime::Cache::new( + wasmtime::CacheConfig::default(), + )?)))?; + + // Load the component - a very expensive operation generating code. Compilation happens in + // parallel and will saturate the entire machine. + let component = { + log::info!("Compiling: {wasm_source_file:?}. May take some time..."); + + let start = SystemTime::now(); + let component = wasmtime::CodeBuilder::new(&engine) + .wasm_binary_or_text_file(&wasm_source_file)? + .compile_component()?; + + // NOTE: According to docs, this shouldn't do anything. + component.initialize_copy_on_write_image()?; + + if let Ok(elapsed) = SystemTime::now().duration_since(start) { + log::info!("Loaded component {wasm_source_file:?} in: {elapsed:?}."); + } + component + }; + + let linker = { + let mut linker = Linker::::new(&engine); + + // Adds all the default WASI implementations: clocks, random, fs, ... + add_to_linker_async(&mut linker)?; + + // Adds default HTTP interfaces - incoming and outgoing. + wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?; + + // Add default KV interfaces. + trailbase_wasi_keyvalue::add_to_linker(&mut linker, |cx| { + trailbase_wasi_keyvalue::WasiKeyValue::new(&cx.kv, &mut cx.resource_table) + })?; + + // Host interfaces. + trailbase::runtime::host_endpoint::add_to_linker::<_, HasSelf>(&mut linker, |s| s)?; + + linker + }; + + log::info!("Starting WASM runtime with {n_threads} threads."); + + let (shared_sender, shared_receiver) = kanal::unbounded_async::(); + let threads = (0..n_threads) + .map(|index| -> Result<_, Error> { + let (private_sender, private_receiver) = kanal::unbounded_async::(); + + let shared_receiver = shared_receiver.clone(); + + let engine = engine.clone(); + let component = component.clone(); + let linker = linker.clone(); + + let conn = conn.clone(); + let kv_store = kv_store.clone(); + let fs_root_path = fs_root_path.clone(); + + let handle = std::thread::Builder::new() + .name(format!("wasm-runtime-{index}")) + .spawn(move || { + // Note: Arc rather than Rc, since State and thus SharedState needs to be Send + Sync. + let tokio_runtime = tokio::runtime::Builder::new_current_thread() + .enable_time() + .enable_io() + .build() + .expect("startup"); + + let shared_state = Arc::new(SharedState { + runtime: tokio_runtime, + conn, + thread_id: index as u64, + kv_store, + fs_root_path, + }); + + let instance = RuntimeInstance { + engine, + component, + linker, + shared: shared_state.clone(), + }; + // RuntimeInstance::new(engine, component, linker, shared_state).expect("startup"); + + event_loop(shared_state, instance, private_receiver, shared_receiver); + }) + .expect("failed to spawn thread"); + + return Ok((handle, private_sender)); + }) + .collect::, Error>>()?; + + return Ok(Self { + shared_sender, + threads, + }); + } + + pub async fn call(&self, f: F) -> Result + where + F: (AsyncFnOnce(&RuntimeInstance) -> O) + Send + 'static, + O: Send + 'static, + { + let (sender, receiver) = tokio::sync::oneshot::channel::(); + + self + .shared_sender + .send(Message::Run(Box::new(move |runtime| { + Box::pin(async move { + let _ = sender.send(f(&*runtime).await); + }) + }))) + .await + .map_err(|_| Error::ChannelClosed)?; + + return receiver.await.map_err(|_| Error::ChannelClosed); + } +} + +fn event_loop( + shared_state: Arc, + instance: RuntimeInstance, + private_recv: kanal::AsyncReceiver, + shared_recv: kanal::AsyncReceiver, +) { + let thread_id = shared_state.thread_id; + let local = tokio::task::LocalSet::new(); + let instance = Rc::new(instance); + + local.block_on(&shared_state.runtime, async move { + let local_in_flight = Rc::new(AtomicUsize::new(0)); + + loop { + let receive_message = async || { + return tokio::select! { + msg = private_recv.recv() => msg, + msg = shared_recv.recv() => msg, + }; + }; + + log::debug!( + "Waiting for new messages (thread: {thread_id}). In flight: {}, {}", + local_in_flight.load(Ordering::Relaxed), + IN_FLIGHT.load(Ordering::Relaxed) + ); + + match receive_message().await { + Ok(Message::Run(f)) => { + let instance = instance.clone(); + + let local_in_flight = local_in_flight.clone(); + local_in_flight.fetch_add(1, Ordering::Relaxed); + + IN_FLIGHT.fetch_add(1, Ordering::Relaxed); + + tokio::task::spawn_local(async move { + f(instance).await; + + IN_FLIGHT.fetch_sub(1, Ordering::Relaxed); + local_in_flight.fetch_sub(1, Ordering::Relaxed); + }); + + // Yield before listening for more messages to give JS a chance to run. + tokio::task::yield_now().await; + } + Err(_) => { + // Channel closed + return; + } + }; + } + }); +} + +pub struct SharedState { + pub thread_id: u64, + pub runtime: tokio::runtime::Runtime, + pub conn: trailbase_sqlite::Connection, + pub kv_store: KvStore, + pub fs_root_path: Option, +} + +pub struct RuntimeInstance { + engine: Engine, + component: Component, + linker: Linker, + + shared: Arc, +} + +impl RuntimeInstance { + // pub fn new( + // engine: Engine, + // component: Component, + // linker: Linker, + // shared_state: SharedState, + // ) -> Result { + // // let mut linker = Linker::::new(&engine); + // // + // // // Adds all the default WASI implementations: clocks, random, fs, ... + // // add_to_linker_async(&mut linker)?; + // // + // // // Adds default HTTP interfaces - incoming and outgoing. + // // wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)?; + // // + // // // Add default KV interfaces. + // // trailbase_wasi_keyvalue::add_to_linker(&mut linker, |cx| { + // // trailbase_wasi_keyvalue::WasiKeyValue::new(&cx.kv, &mut cx.resource_table) + // // })?; + // // + // // // Host interfaces. + // // trailbase::runtime::host_endpoint::add_to_linker::<_, HasSelf>(&mut linker, |s| + // s)?; + // + // return Ok(Self { + // engine, + // component, + // linker, + // shared: Arc::new(shared_state), + // }); + // } + + fn new_store(&self) -> Result, Error> { + let mut wasi_ctx = WasiCtxBuilder::new(); + wasi_ctx.inherit_stdio(); + wasi_ctx.stdin(wasmtime_wasi::p2::pipe::ClosedInputStream); + // wasi_ctx.stdout(wasmtime_wasi::p2::Stdout); + // wasi_ctx.stderr(wasmtime_wasi::p2::Stderr); + + wasi_ctx.args(&[""]); + wasi_ctx.allow_tcp(false); + wasi_ctx.allow_udp(false); + wasi_ctx.allow_ip_name_lookup(true); + + if let Some(ref path) = self.shared.fs_root_path { + wasi_ctx + .preopened_dir(path, "/", DirPerms::READ, FilePerms::READ) + .map_err(|err| Error::Other(err.to_string()))?; + } + + return Ok(Store::new( + &self.engine, + State { + resource_table: ResourceTable::new(), + wasi_ctx: wasi_ctx.build(), + http: WasiHttpCtx::new(), + kv: WasiKeyValueCtx::new(self.shared.kv_store.clone()), + shared: self.shared.clone(), + tx: LockedTransaction(Rc::new(Mutex::new(None))), + }, + )); + } + + pub async fn call_init(&self) -> Result { + let mut store = self.new_store()?; + let bindings = Trailbase::instantiate_async(&mut store, &self.component, &self.linker).await?; + + return Ok( + bindings + .trailbase_runtime_init_endpoint() + .call_init(&mut store) + .await?, + ); + } + + pub async fn call_incoming_http_handler( + &self, + request: hyper::Request>, + ) -> Result, Error> { + let mut store = self.new_store()?; + + let proxy = wasmtime_wasi_http::bindings::Proxy::instantiate_async( + &mut store, + &self.component, + &self.linker, + ) + .await?; + + let req = store.data_mut().new_incoming_request( + wasmtime_wasi_http::bindings::http::types::Scheme::Http, + request, + )?; + + let (sender, receiver) = tokio::sync::oneshot::channel::< + Result, ErrorCode>, + >(); + + let out = store.data_mut().new_response_outparam(sender)?; + + // NOTE: wstd streams out responses in chunks of 2kB. Only once everything has been streamed, + // `call_handle` will complete. This is also when the streaming response body completes. + // + // We cannot use `wasmtime_wasi::runtime::spawn` here, which aborts the call when the handle + // gets dropped, since we're not awaiting the response stream here. We'd either have to consume + // the entire response here, keep the handle alive or as we currently do use a non-aborting + // spawn. + // + // In the current setup, if the listening side hangs-up the they call may not be aborted. + // Depends on what the implementation does when the streaming body's receiving end gets + // out of scope. + let handle = self.shared.runtime.spawn(async move { + proxy + .wasi_http_incoming_handler() + .call_handle(&mut store, req, out) + .await + }); + + return match receiver.await { + Ok(Ok(resp)) => { + // NOTE: We cannot await the completion `call_handle` here with `handle.await?;`, since + // we're not consuming the response body, see above. + Ok(resp) + } + Ok(Err(err)) => { + handle + .await + .map_err(|err| Error::Other(err.to_string()))??; + Err(Error::HttpErrorCode(err)) + } + Err(_) => { + log::debug!("channel closed"); + handle + .await + .map_err(|err| Error::Other(err.to_string()))??; + Err(Error::ChannelClosed) + } + }; + } +} + +#[allow(unused)] +fn bytes_to_respone( + bytes: Vec, +) -> Result { + let resp = http::Response::builder() + .status(200) + .body(sqlite::bytes_to_body(Bytes::from_owner(bytes))) + .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?; + + return Ok( + wasmtime_wasi_http::types::HostFutureIncomingResponse::ready(Ok(Ok( + wasmtime_wasi_http::types::IncomingResponse { + resp, + worker: None, + between_bytes_timeout: std::time::Duration::ZERO, + }, + ))), + ); +} + +fn to_sqlite_value(value: Value) -> trailbase_sqlite::Value { + return match value { + Value::Null => trailbase_sqlite::Value::Null, + Value::Text(s) => trailbase_sqlite::Value::Text(s), + Value::Real(f) => trailbase_sqlite::Value::Real(f), + Value::Integer(i) => trailbase_sqlite::Value::Integer(i), + Value::Blob(b) => trailbase_sqlite::Value::Blob(b), + }; +} + +fn from_sqlite_value(value: trailbase_sqlite::Value) -> Value { + return match value { + trailbase_sqlite::Value::Null => Value::Null, + trailbase_sqlite::Value::Text(s) => Value::Text(s), + trailbase_sqlite::Value::Real(f) => Value::Real(f), + trailbase_sqlite::Value::Integer(i) => Value::Integer(i), + trailbase_sqlite::Value::Blob(b) => Value::Blob(b), + }; +} + +#[cfg(test)] +mod tests { + use super::*; + + use http::{Response, StatusCode}; + use http_body_util::combinators::BoxBody; + use trailbase_wasm_common::{HttpContext, HttpContextKind}; + + #[tokio::test] + async fn test_init() { + let conn = trailbase_sqlite::Connection::open_in_memory().unwrap(); + let kv_store = KvStore::new(); + let runtime = Runtime::new( + 2, + "../../client/testfixture/wasm/wasm_rust_guest_testfixture.wasm".into(), + conn.clone(), + kv_store, + None, + ) + .unwrap(); + + runtime + .call(async |instance| { + instance.call_init().await.unwrap(); + }) + .await + .unwrap(); + + let response = send_http_request( + &runtime, + "http://localhost:4000/transaction", + "/transaction", + ) + .await + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + assert_eq!( + 1, + conn + .query_row_f("SELECT COUNT(*) FROM tx;", (), |row| row.get::<_, i64>(0)) + .await + .unwrap() + .unwrap() + ) + } + + #[tokio::test] + async fn test_transaction() { + let conn = trailbase_sqlite::Connection::open_in_memory().unwrap(); + let kv_store = KvStore::new(); + let runtime = Arc::new( + Runtime::new( + 2, + "../../client/testfixture/wasm/wasm_rust_guest_testfixture.wasm".into(), + conn.clone(), + kv_store, + None, + ) + .unwrap(), + ); + + let futures: Vec<_> = (0..256) + .map(|_| { + let runtime = runtime.clone(); + tokio::spawn(async move { + send_http_request( + &runtime, + "http://localhost:4000/transaction", + "/transaction", + ) + .await + }) + }) + .collect(); + + for future in futures { + future.await.unwrap().unwrap(); + } + } + + async fn send_http_request( + runtime: &Runtime, + uri: &str, + registered_path: &str, + ) -> Result>, Error> { + fn to_header_value(context: &HttpContext) -> hyper::http::HeaderValue { + return hyper::http::HeaderValue::from_bytes( + &serde_json::to_vec(&context).unwrap_or_default(), + ) + .unwrap(); + } + + let uri = uri.to_string(); + let registered_path = registered_path.to_string(); + return runtime + .call(async |instance| { + let context = HttpContext { + kind: HttpContextKind::Http, + registered_path, + path_params: vec![], + user: None, + }; + + let request = hyper::Request::builder() + .uri(uri) + .header("__context", to_header_value(&context)) + .body(sqlite::bytes_to_body(Bytes::from_static(b""))) + .unwrap(); + + return instance.call_incoming_http_handler(request).await; + }) + .await + .unwrap(); + } +} diff --git a/crates/wasm-runtime-host/src/sqlite.rs b/crates/wasm-runtime-host/src/sqlite.rs new file mode 100644 index 00000000..33643a2d --- /dev/null +++ b/crates/wasm-runtime-host/src/sqlite.rs @@ -0,0 +1,225 @@ +use bytes::Bytes; +use http_body_util::{BodyExt, combinators::BoxBody}; +use rusqlite::Transaction; +use self_cell::{MutBorrow, self_cell}; +use tokio::time::Duration; +use trailbase_schema::json::{JsonError, rich_json_to_value, value_to_rich_json}; +use trailbase_sqlite::connection::ArcLockGuard; +use trailbase_wasm_common::{SqliteRequest, SqliteResponse}; +use wasmtime_wasi_http::bindings::http::types::ErrorCode; + +self_cell!( + pub(crate) struct OwnedTx { + owner: MutBorrow, + + #[covariant] + dependent: Transaction, + } +); + +pub(crate) async fn new_tx(conn: trailbase_sqlite::Connection) -> Result { + for _ in 0..200 { + let Some(lock) = conn.try_write_arc_lock_for(Duration::from_micros(100)) else { + tokio::time::sleep(Duration::from_micros(400)).await; + continue; + }; + + return OwnedTx::try_new(MutBorrow::new(lock), |owner| { + return owner.borrow_mut().transaction(); + }); + } + + return Err(rusqlite::Error::ToSqlConversionFailure( + "Failed to acquire lock".into(), + )); +} + +async fn handle_sqlite_request_impl( + conn: trailbase_sqlite::Connection, + request: hyper::Request, +) -> Result { + return match request.uri().path() { + // "/tx_begin" => { + // let new_tx = new_tx(conn).await.map_err(sqlite_err)?; + // + // CURRENT_TX.with(|tx: &Mutex<_>| { + // *tx.lock() = Some(new_tx); + // }); + // + // Ok(SqliteResponse::TxBegin) + // } + // "/tx_commit" => { + // let tx = CURRENT_TX.with(|tx: &Mutex<_>| { + // return tx.lock().take(); + // }); + // if let Some(tx) = tx { + // // NOTE: this is the same as `tx.commit()` just w/o consuming. + // let lock = tx.borrow_dependent(); + // lock.execute_batch("COMMIT").map_err(sqlite_err)?; + // } + // + // Ok(SqliteResponse::TxCommit) + // } + // "/tx_execute" => { + // let sqlite_request = to_request(request).await?; + // + // let params = json_values_to_sqlite_params(sqlite_request.params).map_err(sqlite_err)?; + // + // let rows_affected = CURRENT_TX.with(move |tx: &Mutex<_>| -> Result { + // let Some(ref tx) = *tx.lock() else { + // return Err("No open transaction".to_string()); + // }; + // let lock = tx.borrow_dependent(); + // + // let mut stmt = lock.prepare(&sqlite_request.query).map_err(sqlite_err)?; + // + // params.bind(&mut stmt).map_err(sqlite_err)?; + // + // return stmt.raw_execute().map_err(sqlite_err); + // })?; + // + // Ok(SqliteResponse::Execute { rows_affected }) + // } + // "/tx_query " => { + // let sqlite_request = to_request(request).await?; + // + // let params = json_values_to_sqlite_params(sqlite_request.params).map_err(sqlite_err)?; + // + // let rows = CURRENT_TX.with(move |tx: &Mutex<_>| -> Result { + // let Some(ref tx) = *tx.lock() else { + // return Err("No open transaction".to_string()); + // }; + // let lock = tx.borrow_dependent(); + // + // let mut stmt = lock.prepare(&sqlite_request.query).map_err(sqlite_err)?; + // + // params.bind(&mut stmt).map_err(sqlite_err)?; + // + // return Rows::from_rows(stmt.raw_query()).map_err(sqlite_err); + // })?; + // + // let json_rows = rows + // .iter() + // .map(|row| -> Result, String> { + // return row_to_rich_json_array(row).map_err(sqlite_err); + // }) + // .collect::, _>>()?; + // + // Ok(SqliteResponse::Query { rows: json_rows }) + // } + // "/tx_rollback " => { + // let tx = CURRENT_TX.with(|tx: &Mutex<_>| { + // return tx.lock().take(); + // }); + // if let Some(tx) = tx { + // // NOTE: this is the same as `tx.rollback()` just w/o consuming. + // let lock = tx.borrow_dependent(); + // lock.execute_batch("ROLLBACK").map_err(sqlite_err)?; + // } + // + // Ok(SqliteResponse::TxRollback) + // } + "/execute" => { + let sqlite_request = to_request(request).await?; + + let rows_affected = conn + .execute( + sqlite_request.query, + json_values_to_sqlite_params(sqlite_request.params).map_err(sqlite_err)?, + ) + .await + .map_err(sqlite_err)?; + + Ok(SqliteResponse::Execute { rows_affected }) + } + "/query" => { + let sqlite_request = to_request(request).await?; + + let rows = conn + .write_query_rows( + sqlite_request.query, + json_values_to_sqlite_params(sqlite_request.params).map_err(sqlite_err)?, + ) + .await + .map_err(sqlite_err)?; + + let json_rows = rows + .iter() + .map(|row| -> Result, String> { + return row_to_rich_json_array(row).map_err(sqlite_err); + }) + .collect::, _>>()?; + + Ok(SqliteResponse::Query { rows: json_rows }) + } + _ => Err("Not found".to_string()), + }; +} + +pub(crate) async fn handle_sqlite_request( + conn: trailbase_sqlite::Connection, + request: hyper::Request, +) -> Result { + return match handle_sqlite_request_impl(conn, request).await { + Ok(response) => to_response(response), + Err(err) => to_response(SqliteResponse::Error(err)), + }; +} + +async fn to_request( + request: hyper::Request, +) -> Result { + let (_parts, body) = request.into_parts(); + let bytes: Bytes = body.collect().await.map_err(sqlite_err)?.to_bytes(); + return serde_json::from_slice(&bytes).map_err(sqlite_err); +} + +fn to_response( + response: SqliteResponse, +) -> Result { + let body = + serde_json::to_vec(&response).map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?; + + let resp = http::Response::builder() + .status(200) + .body(bytes_to_body(Bytes::from_owner(body))) + .map_err(|err| ErrorCode::InternalError(Some(err.to_string())))?; + + return Ok(wasmtime_wasi_http::types::IncomingResponse { + resp, + worker: None, + between_bytes_timeout: std::time::Duration::ZERO, + }); +} + +pub(crate) fn json_values_to_sqlite_params( + values: Vec, +) -> Result, JsonError> { + return values.into_iter().map(rich_json_to_value).collect(); +} + +pub fn row_to_rich_json_array( + row: &trailbase_sqlite::Row, +) -> Result, JsonError> { + return (0..row.column_count()) + .map(|i| -> Result { + let value = row.get_value(i).ok_or(JsonError::ValueNotFound)?; + return value_to_rich_json(value); + }) + .collect(); +} + +#[inline] +pub fn bytes_to_body(bytes: Bytes) -> BoxBody { + BoxBody::new(http_body_util::Full::new(bytes).map_err(|_| unreachable!())) +} + +#[inline] +pub fn sqlite_err(err: E) -> String { + return err.to_string(); +} + +// #[inline] +// fn empty() -> BoxBody { +// BoxBody::new(http_body_util::Empty::new().map_err(|_| unreachable!())) +// } diff --git a/crates/wasm-runtime-host/wit b/crates/wasm-runtime-host/wit new file mode 120000 index 00000000..62c86941 --- /dev/null +++ b/crates/wasm-runtime-host/wit @@ -0,0 +1 @@ +../wasm-runtime-guest/wit \ No newline at end of file diff --git a/examples/custom-binary/src/main.rs b/examples/custom-binary/src/main.rs index c4580090..fb062b9a 100644 --- a/examples/custom-binary/src/main.rs +++ b/examples/custom-binary/src/main.rs @@ -46,7 +46,6 @@ async fn main() -> Result<(), Box> { dev: false, disable_auth_ui: false, cors_allowed_origins: vec![], - js_runtime_threads: None, ..Default::default() }, |state: AppState| async move {