From 2df55b7f160120e8f6746e4e77e70b1377a7462c Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Tue, 24 Sep 2024 23:23:54 +0200 Subject: [PATCH 01/23] feat: WIP wasi integrations crate Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/Cargo.toml | 29 + integrations/wasi/Makefile.toml | 4 + integrations/wasi/src/lib.rs | 70 ++ integrations/wasi/src/server_fn.rs | 16 + integrations/wasi/wit/deps.lock | 29 + integrations/wasi/wit/deps.toml | 1 + integrations/wasi/wit/deps/cli/command.wit | 10 + .../wasi/wit/deps/cli/environment.wit | 22 + integrations/wasi/wit/deps/cli/exit.wit | 17 + integrations/wasi/wit/deps/cli/imports.wit | 36 + integrations/wasi/wit/deps/cli/run.wit | 6 + integrations/wasi/wit/deps/cli/stdio.wit | 26 + integrations/wasi/wit/deps/cli/terminal.wit | 62 ++ .../wasi/wit/deps/clocks/monotonic-clock.wit | 50 ++ .../wasi/wit/deps/clocks/timezone.wit | 55 ++ .../wasi/wit/deps/clocks/wall-clock.wit | 46 ++ integrations/wasi/wit/deps/clocks/world.wit | 11 + .../wasi/wit/deps/filesystem/preopens.wit | 11 + .../wasi/wit/deps/filesystem/types.wit | 678 ++++++++++++++++++ .../wasi/wit/deps/filesystem/world.wit | 9 + integrations/wasi/wit/deps/http/handler.wit | 49 ++ integrations/wasi/wit/deps/http/proxy.wit | 50 ++ integrations/wasi/wit/deps/http/types.wit | 655 +++++++++++++++++ integrations/wasi/wit/deps/io/error.wit | 34 + integrations/wasi/wit/deps/io/poll.wit | 47 ++ integrations/wasi/wit/deps/io/streams.wit | 286 ++++++++ integrations/wasi/wit/deps/io/world.wit | 10 + .../wasi/wit/deps/random/insecure-seed.wit | 27 + .../wasi/wit/deps/random/insecure.wit | 25 + integrations/wasi/wit/deps/random/random.wit | 29 + integrations/wasi/wit/deps/random/world.wit | 13 + .../wit/deps/sockets/instance-network.wit | 11 + .../wasi/wit/deps/sockets/ip-name-lookup.wit | 56 ++ .../wasi/wit/deps/sockets/network.wit | 153 ++++ .../wit/deps/sockets/tcp-create-socket.wit | 30 + integrations/wasi/wit/deps/sockets/tcp.wit | 387 ++++++++++ .../wit/deps/sockets/udp-create-socket.wit | 30 + integrations/wasi/wit/deps/sockets/udp.wit | 288 ++++++++ integrations/wasi/wit/deps/sockets/world.wit | 19 + integrations/wasi/wit/world.wit | 5 + 40 files changed, 3392 insertions(+) create mode 100644 integrations/wasi/Cargo.toml create mode 100644 integrations/wasi/Makefile.toml create mode 100644 integrations/wasi/src/lib.rs create mode 100644 integrations/wasi/src/server_fn.rs create mode 100644 integrations/wasi/wit/deps.lock create mode 100644 integrations/wasi/wit/deps.toml create mode 100644 integrations/wasi/wit/deps/cli/command.wit create mode 100644 integrations/wasi/wit/deps/cli/environment.wit create mode 100644 integrations/wasi/wit/deps/cli/exit.wit create mode 100644 integrations/wasi/wit/deps/cli/imports.wit create mode 100644 integrations/wasi/wit/deps/cli/run.wit create mode 100644 integrations/wasi/wit/deps/cli/stdio.wit create mode 100644 integrations/wasi/wit/deps/cli/terminal.wit create mode 100644 integrations/wasi/wit/deps/clocks/monotonic-clock.wit create mode 100644 integrations/wasi/wit/deps/clocks/timezone.wit create mode 100644 integrations/wasi/wit/deps/clocks/wall-clock.wit create mode 100644 integrations/wasi/wit/deps/clocks/world.wit create mode 100644 integrations/wasi/wit/deps/filesystem/preopens.wit create mode 100644 integrations/wasi/wit/deps/filesystem/types.wit create mode 100644 integrations/wasi/wit/deps/filesystem/world.wit create mode 100644 integrations/wasi/wit/deps/http/handler.wit create mode 100644 integrations/wasi/wit/deps/http/proxy.wit create mode 100644 integrations/wasi/wit/deps/http/types.wit create mode 100644 integrations/wasi/wit/deps/io/error.wit create mode 100644 integrations/wasi/wit/deps/io/poll.wit create mode 100644 integrations/wasi/wit/deps/io/streams.wit create mode 100644 integrations/wasi/wit/deps/io/world.wit create mode 100644 integrations/wasi/wit/deps/random/insecure-seed.wit create mode 100644 integrations/wasi/wit/deps/random/insecure.wit create mode 100644 integrations/wasi/wit/deps/random/random.wit create mode 100644 integrations/wasi/wit/deps/random/world.wit create mode 100644 integrations/wasi/wit/deps/sockets/instance-network.wit create mode 100644 integrations/wasi/wit/deps/sockets/ip-name-lookup.wit create mode 100644 integrations/wasi/wit/deps/sockets/network.wit create mode 100644 integrations/wasi/wit/deps/sockets/tcp-create-socket.wit create mode 100644 integrations/wasi/wit/deps/sockets/tcp.wit create mode 100644 integrations/wasi/wit/deps/sockets/udp-create-socket.wit create mode 100644 integrations/wasi/wit/deps/sockets/udp.wit create mode 100644 integrations/wasi/wit/deps/sockets/world.wit create mode 100644 integrations/wasi/wit/world.wit diff --git a/integrations/wasi/Cargo.toml b/integrations/wasi/Cargo.toml new file mode 100644 index 0000000000..c7d001fb9f --- /dev/null +++ b/integrations/wasi/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "leptos_wasi" +authors = ["Enzo Nocera"] +license = "MIT" +repository = "https://github.com/leptos-rs/leptos" +description = "WASI integrations for the Leptos web framework." +version = { workspace = true } +rust-version.workspace = true +edition.workspace = true + +[dependencies] +any_spawner = { workspace = true, features = ["futures-executor"] } +hydration_context = { workspace = true } +futures = "0.3.30" +leptos = { workspace = true, features = ["nonce", "ssr"] } +leptos_meta = { workspace = true, features = ["ssr"] } +leptos_router = { workspace = true, features = ["ssr"] } +leptos_integration_utils = { workspace = true } +once_cell = "1" +serde_json = "1.0" +wit-bindgen = "0.32.0" +http = "1.1.0" +parking_lot = "0.12.3" + +[package.metadata.docs.rs] +rustdoc-args = ["--generate-link-to-definition"] + +[package.metadata.cargo-all-features] +denylist = ["tracing"] diff --git a/integrations/wasi/Makefile.toml b/integrations/wasi/Makefile.toml new file mode 100644 index 0000000000..4ed6229141 --- /dev/null +++ b/integrations/wasi/Makefile.toml @@ -0,0 +1,4 @@ +extend = { path = "../../cargo-make/main.toml" } + +[tasks.check-format] +env = { LEPTOS_PROJECT_DIRECTORY = "../../" } diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs new file mode 100644 index 0000000000..4912d380a7 --- /dev/null +++ b/integrations/wasi/src/lib.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use bindings::wasi::http::types::{Fields, IncomingRequest, OutgoingResponse}; +use http::{HeaderMap, StatusCode, HeaderName, HeaderValue}; +use hydration_context::PinnedStream; +use parking_lot::RwLock; + +pub mod bindings { + wit_bindgen::generate!({ + path: "wit", + pub_export_macro: true, + world: "http", + generate_all, + }); +} + +pub mod server_fn; + +pub struct WasiRequest(pub IncomingRequest); + +pub struct WasiResponse { + fields: Fields, + resp: OutgoingResponse, + + /// Optional stream to consume to produce the response, + /// the tachys crate seems to produce String stream so we use + /// the same here. If it is set, the stream is consumed and the + /// chunks are appended to the body of resp. + stream: Option>, +} + +/// This struct lets you define headers and override the status of the Response from an Element or a Server Function +/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. +#[derive(Debug, Clone, Default)] +pub struct ResponseParts { + pub headers: HeaderMap, + pub status: Option, +} + +/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies. +#[derive(Debug, Clone, Default)] +pub struct ResponseOptions(Arc>); + +impl ResponseOptions { + /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`. + pub fn overwrite(&self, parts: ResponseParts) { + let mut writable = self.0.write(); + *writable = parts + } + /// Set the status of the returned Response. + pub fn set_status(&self, status: StatusCode) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.status = Some(status); + } + /// Insert a header, overwriting any previous value with the same key. + pub fn insert_header(&self, key: HeaderName, value: HeaderValue) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.headers.insert(key, value); + } + /// Append a header, leaving any header with the same key intact. + pub fn append_header(&self, key: HeaderName, value: HeaderValue) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.headers.append(key, value); + } +} + + diff --git a/integrations/wasi/src/server_fn.rs b/integrations/wasi/src/server_fn.rs new file mode 100644 index 0000000000..b28db78e35 --- /dev/null +++ b/integrations/wasi/src/server_fn.rs @@ -0,0 +1,16 @@ +#![forbid(unsafe_code)] + +//! A simplification of Leptos Server Functions inventory. +//! Since it's not possible in Rust to use most crates relying on +//! linker sections when targeting `wasm32` (e.g. `inventory` crate), +//! and since our component's lifetime is bound to the one of the +//! incoming request, we can simplificate the codebase a lot. + +use leptos::server_fn::ServerFnTraitObj; + +use crate::{WasiRequest, WasiResponse}; + +pub enum Matcher { + Found(ServerFnTraitObj), + NotFound, +} diff --git a/integrations/wasi/wit/deps.lock b/integrations/wasi/wit/deps.lock new file mode 100644 index 0000000000..463fc62133 --- /dev/null +++ b/integrations/wasi/wit/deps.lock @@ -0,0 +1,29 @@ +[cli] +sha256 = "1de50b8e6940e73110cda10b7f90ca87a8fea886f0fa36c748f96dc70671ee38" +sha512 = "bbb6cd3e7b4d3237b6af9bfbb2633ccd2c4ea2a4f37b8c033255c7e0c1cb037be7f22ec1f8ca792cc8ec1942199582943979e646b4b272b85dcff7654eac51d0" + +[clocks] +sha256 = "ea9d69ee803bc176e23e5268f5e24a2ac485dd1f62a0ab4c748e9d3f901f576f" +sha512 = "5efc22927c46cd56c41e5549ec775561c7fac2ea0d365abc0b55396d9475a7c9f984077a81f84a44a726f1c008fd2fadbffffa4fa53ecd5fbfd05afd379ab428" + +[filesystem] +sha256 = "cfe8c420e8b857de612ae2a3336680dae16b95c93c8ba3a6ff05b21210966740" +sha512 = "3c00c5544a58658e3e8025677091685286027fd49f37abf198c30b4e83b9e68f19723975aaa98794fba9f425ae9ef4f3dc0f5b9cf59203b5ecfaadf62b296f9a" + +[http] +url = "https://github.com/WebAssembly/wasi-http/archive/v0.2.1.tar.gz" +sha256 = "ff21e05571af02488371bebc9cb674e30e0613eda22e3c1d8e1aba36cdff5ee8" +sha512 = "1825b56f1718e822acf1b49929ead8f6493752b1d3524ce0974c3acdf656af2fc3fe5c8456b71ffab44583bc3ae7061d5a285d8a7203fcae949d44b3d81e2f2b" +deps = ["cli", "clocks", "filesystem", "io", "random", "sockets"] + +[io] +sha256 = "2a74bd811adc46b5a0f19827ddbde89870e52b17615f4d0873f06fd977250caf" +sha512 = "94624f00c66e66203592cee820f80b1ba91ecdb71f682c154f25eaf71f8d8954197dcb64503bc21e72ed5e812af7eae876df47b7eb727b02db3a74a7ce0aefca" + +[random] +sha256 = "9e2d5056186f81b2e7f96bc97d2babd0341840f6abb4f170449b70992f1b598f" +sha512 = "67bf41d8d5d4b7af084124ee85196585225785969059f59e2f9ddb77ac1a8095cfe811ae29d076aac817418fa01064d7b9fbc0233930bace680758eeb21e36f8" + +[sockets] +sha256 = "4c361137a7e61e8b9a73da2a0899dd9ad1a0c2dfee7d310cf168704c57b7a07c" +sha512 = "348b4ef381f57aec23d48537df8b69ab8963587dcb056e94c4cd5657e217677a4ee2a545868a5c829d2334cc6b8b0a61d3e72797999f44d78553fbd3a73c5b8d" diff --git a/integrations/wasi/wit/deps.toml b/integrations/wasi/wit/deps.toml new file mode 100644 index 0000000000..77a7d114c2 --- /dev/null +++ b/integrations/wasi/wit/deps.toml @@ -0,0 +1 @@ +http = "https://github.com/WebAssembly/wasi-http/archive/v0.2.1.tar.gz" diff --git a/integrations/wasi/wit/deps/cli/command.wit b/integrations/wasi/wit/deps/cli/command.wit new file mode 100644 index 0000000000..dc064a3cd9 --- /dev/null +++ b/integrations/wasi/wit/deps/cli/command.wit @@ -0,0 +1,10 @@ +package wasi:cli@0.2.1; + +@since(version = 0.2.0) +world command { + @since(version = 0.2.0) + include imports; + + @since(version = 0.2.0) + export run; +} diff --git a/integrations/wasi/wit/deps/cli/environment.wit b/integrations/wasi/wit/deps/cli/environment.wit new file mode 100644 index 0000000000..2f449bd7c1 --- /dev/null +++ b/integrations/wasi/wit/deps/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/integrations/wasi/wit/deps/cli/exit.wit b/integrations/wasi/wit/deps/cli/exit.wit new file mode 100644 index 0000000000..427935c8d0 --- /dev/null +++ b/integrations/wasi/wit/deps/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/integrations/wasi/wit/deps/cli/imports.wit b/integrations/wasi/wit/deps/cli/imports.wit new file mode 100644 index 0000000000..b8339d3b22 --- /dev/null +++ b/integrations/wasi/wit/deps/cli/imports.wit @@ -0,0 +1,36 @@ +package wasi:cli@0.2.1; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + include wasi:clocks/imports@0.2.1; + @since(version = 0.2.0) + include wasi:filesystem/imports@0.2.1; + @since(version = 0.2.0) + include wasi:sockets/imports@0.2.1; + @since(version = 0.2.0) + include wasi:random/imports@0.2.1; + @since(version = 0.2.0) + include wasi:io/imports@0.2.1; + + @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/integrations/wasi/wit/deps/cli/run.wit b/integrations/wasi/wit/deps/cli/run.wit new file mode 100644 index 0000000000..655346efb6 --- /dev/null +++ b/integrations/wasi/wit/deps/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/integrations/wasi/wit/deps/cli/stdio.wit b/integrations/wasi/wit/deps/cli/stdio.wit new file mode 100644 index 0000000000..d1d26eb615 --- /dev/null +++ b/integrations/wasi/wit/deps/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.1.{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.1.{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.1.{output-stream}; + + @since(version = 0.2.0) + get-stderr: func() -> output-stream; +} diff --git a/integrations/wasi/wit/deps/cli/terminal.wit b/integrations/wasi/wit/deps/cli/terminal.wit new file mode 100644 index 0000000000..d305498c64 --- /dev/null +++ b/integrations/wasi/wit/deps/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/integrations/wasi/wit/deps/clocks/monotonic-clock.wit b/integrations/wasi/wit/deps/clocks/monotonic-clock.wit new file mode 100644 index 0000000000..3c24840c98 --- /dev/null +++ b/integrations/wasi/wit/deps/clocks/monotonic-clock.wit @@ -0,0 +1,50 @@ +package wasi:clocks@0.2.1; +/// 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.1.{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/integrations/wasi/wit/deps/clocks/timezone.wit b/integrations/wasi/wit/deps/clocks/timezone.wit new file mode 100644 index 0000000000..212da66821 --- /dev/null +++ b/integrations/wasi/wit/deps/clocks/timezone.wit @@ -0,0 +1,55 @@ +package wasi:clocks@0.2.1; + +@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/integrations/wasi/wit/deps/clocks/wall-clock.wit b/integrations/wasi/wit/deps/clocks/wall-clock.wit new file mode 100644 index 0000000000..6be069a327 --- /dev/null +++ b/integrations/wasi/wit/deps/clocks/wall-clock.wit @@ -0,0 +1,46 @@ +package wasi:clocks@0.2.1; +/// 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/integrations/wasi/wit/deps/clocks/world.wit b/integrations/wasi/wit/deps/clocks/world.wit new file mode 100644 index 0000000000..9251ac6456 --- /dev/null +++ b/integrations/wasi/wit/deps/clocks/world.wit @@ -0,0 +1,11 @@ +package wasi:clocks@0.2.1; + +@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/integrations/wasi/wit/deps/filesystem/preopens.wit b/integrations/wasi/wit/deps/filesystem/preopens.wit new file mode 100644 index 0000000000..ca2f726af9 --- /dev/null +++ b/integrations/wasi/wit/deps/filesystem/preopens.wit @@ -0,0 +1,11 @@ +package wasi:filesystem@0.2.1; + +@since(version = 0.2.0) +interface preopens { + @since(version = 0.2.0) + use types.{descriptor}; + + /// Return the set of preopened directories, and their path. + @since(version = 0.2.0) + get-directories: func() -> list>; +} diff --git a/integrations/wasi/wit/deps/filesystem/types.wit b/integrations/wasi/wit/deps/filesystem/types.wit new file mode 100644 index 0000000000..db3d96867b --- /dev/null +++ b/integrations/wasi/wit/deps/filesystem/types.wit @@ -0,0 +1,678 @@ +package wasi:filesystem@0.2.1; +/// 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.1.{input-stream, output-stream, error}; + @since(version = 0.2.0) + use wasi:clocks/wall-clock@0.2.1.{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 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. + /// + /// 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. + /// + /// The returned descriptor is not guaranteed to be the lowest-numbered + /// descriptor not currently open/ it is randomized to prevent applications + /// from depending on making assumptions about indexes, since this is + /// error-prone in multi-threaded contexts. The returned descriptor is + /// guaranteed to be less than 2**31. + /// + /// 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 encourated 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/integrations/wasi/wit/deps/filesystem/world.wit b/integrations/wasi/wit/deps/filesystem/world.wit new file mode 100644 index 0000000000..af0146cbc9 --- /dev/null +++ b/integrations/wasi/wit/deps/filesystem/world.wit @@ -0,0 +1,9 @@ +package wasi:filesystem@0.2.1; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import types; + @since(version = 0.2.0) + import preopens; +} diff --git a/integrations/wasi/wit/deps/http/handler.wit b/integrations/wasi/wit/deps/http/handler.wit new file mode 100644 index 0000000000..6a6c62966f --- /dev/null +++ b/integrations/wasi/wit/deps/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/integrations/wasi/wit/deps/http/proxy.wit b/integrations/wasi/wit/deps/http/proxy.wit new file mode 100644 index 0000000000..415d2ee1cb --- /dev/null +++ b/integrations/wasi/wit/deps/http/proxy.wit @@ -0,0 +1,50 @@ +package wasi:http@0.2.1; + +/// 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.1; + @since(version = 0.2.0) + import wasi:clocks/wall-clock@0.2.1; + @since(version = 0.2.0) + import wasi:random/random@0.2.1; + + /// 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.1; + @since(version = 0.2.0) + import wasi:cli/stderr@0.2.1; + + /// 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.1; + + /// 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/integrations/wasi/wit/deps/http/types.wit b/integrations/wasi/wit/deps/http/types.wit new file mode 100644 index 0000000000..3c45cd08b7 --- /dev/null +++ b/integrations/wasi/wit/deps/http/types.wit @@ -0,0 +1,655 @@ +/// 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.1.{duration}; + @since(version = 0.2.0) + use wasi:io/streams@0.2.1.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/error@0.2.1.{error as io-error}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.1.{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: + /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-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-key` 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-key` 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 keys are always strings. + @since(version = 0.2.0) + 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 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 key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + /// + /// The tuple is a pair of the field key, represented as a string, and + /// Value, represented as a list of bytes. + /// + /// An error result will be returned if any `field-key` 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 key. If the key is not present + /// in this `fields` or is syntactically invalid, an empty list is returned. + /// However, if the key 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-key) -> list; + + /// Returns `true` when the key is present in this `fields`. If the key is + /// syntactically invalid, `false` is returned. + @since(version = 0.2.0) + has: func(name: field-key) -> bool; + + /// Set all of the values for a key. Clears any existing values for that + /// key, if they have been set. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-key` or any of + /// the `field-value`s are syntactically invalid. + @since(version = 0.2.0) + set: func(name: field-key, value: list) -> result<_, header-error>; + + /// Delete all values for a key. Does nothing if no values for the key + /// exist. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-key` is + /// syntactically invalid. + @since(version = 0.2.0) + delete: func(name: field-key) -> result<_, header-error>; + + /// Append a value for a key. Does not change or delete any existing + /// values for that key. + /// + /// Fails with `header-error.immutable` if the `fields` are immutable. + /// + /// Fails with `header-error.invalid-syntax` if the `field-key` or + /// `field-value` are syntactically invalid. + @since(version = 0.2.0) + append: func(name: field-key, value: field-value) -> result<_, header-error>; + + /// Retrieve the full set of keys and values in the Fields. Like the + /// constructor, the list represents each key-value pair. + /// + /// The outer list represents each key-value pair in the Fields. Keys + /// which have multiple values are represented by multiple entries in this + /// list with the same key. + @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 { + + /// 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/integrations/wasi/wit/deps/io/error.wit b/integrations/wasi/wit/deps/io/error.wit new file mode 100644 index 0000000000..4ea29c4696 --- /dev/null +++ b/integrations/wasi/wit/deps/io/error.wit @@ -0,0 +1,34 @@ +package wasi:io@0.2.1; + +@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/integrations/wasi/wit/deps/io/poll.wit b/integrations/wasi/wit/deps/io/poll.wit new file mode 100644 index 0000000000..b25ac729ff --- /dev/null +++ b/integrations/wasi/wit/deps/io/poll.wit @@ -0,0 +1,47 @@ +package wasi:io@0.2.1; + +/// 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/integrations/wasi/wit/deps/io/streams.wit b/integrations/wasi/wit/deps/io/streams.wit new file mode 100644 index 0000000000..b697e24d61 --- /dev/null +++ b/integrations/wasi/wit/deps/io/streams.wit @@ -0,0 +1,286 @@ +package wasi:io@0.2.1; + +/// 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. + 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. + 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/integrations/wasi/wit/deps/io/world.wit b/integrations/wasi/wit/deps/io/world.wit new file mode 100644 index 0000000000..6405a4e482 --- /dev/null +++ b/integrations/wasi/wit/deps/io/world.wit @@ -0,0 +1,10 @@ +package wasi:io@0.2.1; + +@since(version = 0.2.0) +world imports { + @since(version = 0.2.0) + import streams; + + @since(version = 0.2.0) + import poll; +} diff --git a/integrations/wasi/wit/deps/random/insecure-seed.wit b/integrations/wasi/wit/deps/random/insecure-seed.wit new file mode 100644 index 0000000000..7e708dc52b --- /dev/null +++ b/integrations/wasi/wit/deps/random/insecure-seed.wit @@ -0,0 +1,27 @@ +package wasi:random@0.2.1; +/// 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/integrations/wasi/wit/deps/random/insecure.wit b/integrations/wasi/wit/deps/random/insecure.wit new file mode 100644 index 0000000000..3cdb53dfbb --- /dev/null +++ b/integrations/wasi/wit/deps/random/insecure.wit @@ -0,0 +1,25 @@ +package wasi:random@0.2.1; +/// 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/integrations/wasi/wit/deps/random/random.wit b/integrations/wasi/wit/deps/random/random.wit new file mode 100644 index 0000000000..2b5035d1ce --- /dev/null +++ b/integrations/wasi/wit/deps/random/random.wit @@ -0,0 +1,29 @@ +package wasi:random@0.2.1; +/// 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/integrations/wasi/wit/deps/random/world.wit b/integrations/wasi/wit/deps/random/world.wit new file mode 100644 index 0000000000..c615e96dc7 --- /dev/null +++ b/integrations/wasi/wit/deps/random/world.wit @@ -0,0 +1,13 @@ +package wasi:random@0.2.1; + +@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/integrations/wasi/wit/deps/sockets/instance-network.wit b/integrations/wasi/wit/deps/sockets/instance-network.wit new file mode 100644 index 0000000000..5f6e6c1cc9 --- /dev/null +++ b/integrations/wasi/wit/deps/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/integrations/wasi/wit/deps/sockets/ip-name-lookup.wit b/integrations/wasi/wit/deps/sockets/ip-name-lookup.wit new file mode 100644 index 0000000000..dc56f30007 --- /dev/null +++ b/integrations/wasi/wit/deps/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.1.{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 Preview2 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/integrations/wasi/wit/deps/sockets/network.wit b/integrations/wasi/wit/deps/sockets/network.wit new file mode 100644 index 0000000000..8c13b348e5 --- /dev/null +++ b/integrations/wasi/wit/deps/sockets/network.wit @@ -0,0 +1,153 @@ +@since(version = 0.2.0) +interface network { + /// 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, + } + + @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/integrations/wasi/wit/deps/sockets/tcp-create-socket.wit b/integrations/wasi/wit/deps/sockets/tcp-create-socket.wit new file mode 100644 index 0000000000..eedbd30768 --- /dev/null +++ b/integrations/wasi/wit/deps/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/integrations/wasi/wit/deps/sockets/tcp.wit b/integrations/wasi/wit/deps/sockets/tcp.wit new file mode 100644 index 0000000000..bae5a29ece --- /dev/null +++ b/integrations/wasi/wit/deps/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.1.{input-stream, output-stream}; + @since(version = 0.2.0) + use wasi:io/poll@0.2.1.{pollable}; + @since(version = 0.2.0) + use wasi:clocks/monotonic-clock@0.2.1.{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 Preview2 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/integrations/wasi/wit/deps/sockets/udp-create-socket.wit b/integrations/wasi/wit/deps/sockets/udp-create-socket.wit new file mode 100644 index 0000000000..e8eeacbfef --- /dev/null +++ b/integrations/wasi/wit/deps/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/integrations/wasi/wit/deps/sockets/udp.wit b/integrations/wasi/wit/deps/sockets/udp.wit new file mode 100644 index 0000000000..b289e4943f --- /dev/null +++ b/integrations/wasi/wit/deps/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.1.{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 Preview2 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 Preview2 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 Preview2 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/integrations/wasi/wit/deps/sockets/world.wit b/integrations/wasi/wit/deps/sockets/world.wit new file mode 100644 index 0000000000..a1d42670e6 --- /dev/null +++ b/integrations/wasi/wit/deps/sockets/world.wit @@ -0,0 +1,19 @@ +package wasi:sockets@0.2.1; + +@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/integrations/wasi/wit/world.wit b/integrations/wasi/wit/world.wit new file mode 100644 index 0000000000..4889b8f16c --- /dev/null +++ b/integrations/wasi/wit/world.wit @@ -0,0 +1,5 @@ +package leptos:server; + +world http { + include wasi:http/proxy@0.2.1; +} From 59bebfa1678e233bf53f7d8ccb217edaec2f72f0 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sat, 28 Sep 2024 15:06:41 +0200 Subject: [PATCH 02/23] feat(server_fn): add generic types This commit adds `From` implementations for the `Req` and `Res` types using abstraction that are deemed "platform-agnostic". Indeed, both the `http` and `bytes` crates contains types that allows us to represent HTTP Request and Response, while being capable to target unconventional platforms (they even have `no-std` support). This allows the server_fn functions to target new platforms, for example, the `wasm32-wasip*` targets. Signed-off-by: Enzo "raskyld" Nocera --- leptos_macro/Cargo.toml | 2 + server_fn/Cargo.toml | 14 +++++ server_fn/src/lib.rs | 9 ++++ server_fn/src/request/generic.rs | 63 ++++++++++++++++++++++ server_fn/src/request/mod.rs | 3 ++ server_fn/src/response/generic.rs | 86 +++++++++++++++++++++++++++++++ server_fn/src/response/mod.rs | 3 ++ server_fn_macro/Cargo.toml | 2 + server_fn_macro/src/lib.rs | 12 ++++- 9 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 server_fn/src/request/generic.rs create mode 100644 server_fn/src/response/generic.rs diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 395dd2e0cf..596122072b 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -48,6 +48,8 @@ experimental-islands = [] trace-component-props = [] actix = ["server_fn_macro/actix"] axum = ["server_fn_macro/axum"] +generic = ["server_fn_macro/generic"] +wasi = ["server_fn_macro/wasi"] [package.metadata.cargo-all-features] denylist = ["nightly", "tracing", "trace-component-props"] diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 745ca291f0..9d2bb2c9d9 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -110,6 +110,8 @@ default-tls = ["reqwest?/default-tls"] rustls = ["reqwest?/rustls-tls"] reqwest = ["dep:reqwest"] ssr = ["inventory"] +wasi = ["generic"] +generic = [] [package.metadata.docs.rs] all-features = true @@ -138,6 +140,10 @@ skip_feature_sets = [ "actix", "axum", ], + [ + "actix", + "generic", + ], [ "browser", "actix", @@ -150,6 +156,10 @@ skip_feature_sets = [ "browser", "reqwest", ], + [ + "browser", + "generic", + ], [ "default-tls", "rustls", @@ -166,6 +176,10 @@ skip_feature_sets = [ "axum-no-default", "browser", ], + [ + "axum-no-default", + "generic", + ], [ "rkyv", "json", diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index a3e67ad459..3b17e52bac 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -117,9 +117,18 @@ pub mod response; #[cfg(feature = "actix")] #[doc(hidden)] pub use ::actix_web as actix_export; + #[cfg(feature = "axum-no-default")] #[doc(hidden)] pub use ::axum as axum_export; + +#[cfg(feature = "generic")] +#[doc(hidden)] +pub use ::http as http_export; +#[cfg(feature = "generic")] +#[doc(hidden)] +pub use ::bytes as bytes_export; + use client::Client; use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; #[doc(hidden)] diff --git a/server_fn/src/request/generic.rs b/server_fn/src/request/generic.rs new file mode 100644 index 0000000000..adba07558b --- /dev/null +++ b/server_fn/src/request/generic.rs @@ -0,0 +1,63 @@ +use std::borrow::Cow; + +use bytes::Bytes; +use futures::{stream::{self, Stream}, StreamExt}; +use http::Request; + +use crate::request::Req; + +impl Req for Request +where + CustErr: 'static, +{ + async fn try_into_bytes( + self, + ) -> Result> { + Ok(self.into_body()) + } + + async fn try_into_string( + self, + ) -> Result> { + String::from_utf8(self.into_body().into()) + .map_err(|err| crate::ServerFnError::Deserialization(err.to_string())) + } + + fn try_into_stream( + self, + ) -> Result< + impl Stream> + Send + 'static, + crate::ServerFnError, + > { + Ok( + stream::iter(self.into_body().into_iter()) + .ready_chunks(16) + .map(|chunk| Ok(Bytes::from(chunk))) + ) + } + + fn to_content_type(&self) -> Option> { + self + .headers() + .get(http::header::CONTENT_TYPE) + .map(|val| String::from_utf8_lossy(val.as_bytes())) + } + + fn accepts(&self) -> Option> { + self + .headers() + .get(http::header::ACCEPT) + .map(|val| String::from_utf8_lossy(val.as_bytes())) + } + + fn referer(&self) -> Option> { + self + .headers() + .get(http::header::REFERER) + .map(|val| String::from_utf8_lossy(val.as_bytes())) + } + + fn as_query(&self) -> Option<&str> { + self.uri().query() + } +} \ No newline at end of file diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index 2f68c8930f..af5171bcd7 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -15,6 +15,9 @@ pub mod browser; /// Request types for [`reqwest`]. #[cfg(feature = "reqwest")] pub mod reqwest; +/// Generic request type +#[cfg(feature = "generic")] +pub mod generic; /// Represents a request as made by the client. pub trait ClientReq diff --git a/server_fn/src/response/generic.rs b/server_fn/src/response/generic.rs new file mode 100644 index 0000000000..06b305908e --- /dev/null +++ b/server_fn/src/response/generic.rs @@ -0,0 +1,86 @@ +use super::Res; +use crate::error::{ + NoCustomError, ServerFnError, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER +}; +use bytes::Bytes; +use futures::Stream; +use http::{header, HeaderValue, Response, StatusCode}; +use std::{ + fmt::{Debug, Display}, pin::Pin, str::FromStr +}; + +/// The Body of a Response whose *execution model* can be +/// customised using the variants. +pub enum Body { + /// The response body will be written synchronously. + Sync(Bytes), + + /// The response body will be written asynchronously, + /// this execution model is also known as + /// "streaming". + Async(Pin>> + 'static>>), +} + +impl From for Body { + fn from(value: String) -> Self { + Body::Sync(Bytes::from(value)) + } +} + +impl Res for Response> +where + CustErr: Send + Sync + Debug + FromStr + Display + 'static, +{ + fn try_from_string( + content_type: &str, + data: String, + ) -> Result> { + let builder = http::Response::builder(); + builder + .status(200) + .header(http::header::CONTENT_TYPE, content_type) + .body(data.into()) + .map_err(|e| ServerFnError::Response(e.to_string())) + } + + fn try_from_bytes( + content_type: &str, + data: Bytes, + ) -> Result> { + let builder = http::Response::builder(); + builder + .status(200) + .header(http::header::CONTENT_TYPE, content_type) + .body(Body::Sync(data)) + .map_err(|e| ServerFnError::Response(e.to_string())) + } + + fn try_from_stream( + content_type: &str, + data: impl Stream>> + + Send + + 'static, + ) -> Result> { + let builder = http::Response::builder(); + builder + .status(200) + .header(http::header::CONTENT_TYPE, content_type) + .body(Body::Async(Box::pin(data))) + .map_err(|e| ServerFnError::Response(e.to_string())) + } + + fn error_response(path: &str, err: &ServerFnError) -> Self { + Response::builder() + .status(http::StatusCode::INTERNAL_SERVER_ERROR) + .header(SERVER_FN_ERROR_HEADER, path) + .body(err.ser().unwrap_or_else(|_| err.to_string()).into()) + .unwrap() + } + + fn redirect(&mut self, path: &str) { + if let Ok(path) = HeaderValue::from_str(path) { + self.headers_mut().insert(header::LOCATION, path); + *self.status_mut() = StatusCode::FOUND; + } + } +} diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index 9515d711a6..d62a85ea52 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -10,6 +10,9 @@ pub mod http; /// Response types for [`reqwest`]. #[cfg(feature = "reqwest")] pub mod reqwest; +/// Generic request type +#[cfg(feature = "generic")] +pub mod generic; use crate::error::ServerFnError; use bytes::Bytes; diff --git a/server_fn_macro/Cargo.toml b/server_fn_macro/Cargo.toml index b14343fb02..b12a1823fa 100644 --- a/server_fn_macro/Cargo.toml +++ b/server_fn_macro/Cargo.toml @@ -21,6 +21,8 @@ nightly = [] ssr = [] actix = [] axum = [] +wasi = ["generic"] +generic = [] reqwest = [] [package.metadata.docs.rs] diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 896f5f2c16..1e80222a8e 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -527,12 +527,16 @@ pub fn server_macro_impl( } } else if cfg!(feature = "axum") { quote! { - #server_fn_path::axum_export::http::Request<#server_fn_path::axum_export::body::Body> + #server_fn_path::http_export::Request<#server_fn_path::axum_export::body::Body> } } else if cfg!(feature = "actix") { quote! { #server_fn_path::request::actix::ActixRequest } + } else if cfg!(feature = "generic") { + quote! { + #server_fn_path::http_export::Request<#server_fn_path::bytes_export::Bytes> + } } else if let Some(req_ty) = req_ty { req_ty.to_token_stream() } else if let Some(req_ty) = preset_req { @@ -551,12 +555,16 @@ pub fn server_macro_impl( } } else if cfg!(feature = "axum") { quote! { - #server_fn_path::axum_export::http::Response<#server_fn_path::axum_export::body::Body> + #server_fn_path::http_export::Response<#server_fn_path::axum_export::body::Body> } } else if cfg!(feature = "actix") { quote! { #server_fn_path::response::actix::ActixResponse } + } else if cfg!(feature = "generic") { + quote! { + #server_fn_path::http_export::Response<#server_fn_path::response::generic::Body> + } } else if let Some(res_ty) = res_ty { res_ty.to_token_stream() } else if let Some(res_ty) = preset_res { From 86812e5d2427ad9dbba3127a01ad6f1863f6db25 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sat, 5 Oct 2024 12:22:14 +0200 Subject: [PATCH 03/23] chore(server_fn): generic types cleanup Signed-off-by: Enzo "raskyld" Nocera --- server_fn/src/lib.rs | 4 +- server_fn/src/request/generic.rs | 61 +++++++++++++++++++------------ server_fn/src/request/mod.rs | 5 +-- server_fn/src/response/generic.rs | 46 ++++++++++++++++++----- server_fn/src/response/mod.rs | 5 +-- 5 files changed, 80 insertions(+), 41 deletions(-) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 3b17e52bac..6d960b4a28 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -124,10 +124,10 @@ pub use ::axum as axum_export; #[cfg(feature = "generic")] #[doc(hidden)] -pub use ::http as http_export; +pub use ::bytes as bytes_export; #[cfg(feature = "generic")] #[doc(hidden)] -pub use ::bytes as bytes_export; +pub use ::http as http_export; use client::Client; use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; diff --git a/server_fn/src/request/generic.rs b/server_fn/src/request/generic.rs index adba07558b..c8eef2d3f6 100644 --- a/server_fn/src/request/generic.rs +++ b/server_fn/src/request/generic.rs @@ -1,7 +1,24 @@ +//! This module uses platform-agnostic abstractions +//! allowing users to run server functions on a wide range of +//! platforms. +//! +//! The crates in use in this crate are: +//! +//! * `bytes`: platform-agnostic manipulation of bytes. +//! * `http`: low-dependency HTTP abstractions' *front-end*. +//! +//! # Users +//! +//! * `wasm32-wasip*` integration crate `leptos_wasi` is using this +//! crate under the hood. + use std::borrow::Cow; use bytes::Bytes; -use futures::{stream::{self, Stream}, StreamExt}; +use futures::{ + stream::{self, Stream}, + StreamExt, +}; use http::Request; use crate::request::Req; @@ -11,48 +28,44 @@ where CustErr: 'static, { async fn try_into_bytes( - self, - ) -> Result> { + self, + ) -> Result> { Ok(self.into_body()) } async fn try_into_string( - self, - ) -> Result> { - String::from_utf8(self.into_body().into()) - .map_err(|err| crate::ServerFnError::Deserialization(err.to_string())) + self, + ) -> Result> { + String::from_utf8(self.into_body().into()).map_err(|err| { + crate::ServerFnError::Deserialization(err.to_string()) + }) } fn try_into_stream( - self, - ) -> Result< - impl Stream> + Send + 'static, - crate::ServerFnError, - > { - Ok( - stream::iter(self.into_body().into_iter()) - .ready_chunks(16) - .map(|chunk| Ok(Bytes::from(chunk))) - ) + self, + ) -> Result< + impl Stream> + Send + 'static, + crate::ServerFnError, + > { + Ok(stream::iter(self.into_body().into_iter()) + .ready_chunks(16) + .map(|chunk| Ok(Bytes::from(chunk)))) } fn to_content_type(&self) -> Option> { - self - .headers() + self.headers() .get(http::header::CONTENT_TYPE) .map(|val| String::from_utf8_lossy(val.as_bytes())) } fn accepts(&self) -> Option> { - self - .headers() + self.headers() .get(http::header::ACCEPT) .map(|val| String::from_utf8_lossy(val.as_bytes())) } fn referer(&self) -> Option> { - self - .headers() + self.headers() .get(http::header::REFERER) .map(|val| String::from_utf8_lossy(val.as_bytes())) } @@ -60,4 +73,4 @@ where fn as_query(&self) -> Option<&str> { self.uri().query() } -} \ No newline at end of file +} diff --git a/server_fn/src/request/mod.rs b/server_fn/src/request/mod.rs index af5171bcd7..3a4c71d393 100644 --- a/server_fn/src/request/mod.rs +++ b/server_fn/src/request/mod.rs @@ -12,12 +12,11 @@ pub mod axum; /// Request types for the browser. #[cfg(feature = "browser")] pub mod browser; +#[cfg(feature = "generic")] +pub mod generic; /// Request types for [`reqwest`]. #[cfg(feature = "reqwest")] pub mod reqwest; -/// Generic request type -#[cfg(feature = "generic")] -pub mod generic; /// Represents a request as made by the client. pub trait ClientReq diff --git a/server_fn/src/response/generic.rs b/server_fn/src/response/generic.rs index 06b305908e..5b95dfef72 100644 --- a/server_fn/src/response/generic.rs +++ b/server_fn/src/response/generic.rs @@ -1,33 +1,57 @@ +//! This module uses platform-agnostic abstractions +//! allowing users to run server functions on a wide range of +//! platforms. +//! +//! The crates in use in this crate are: +//! +//! * `bytes`: platform-agnostic manipulation of bytes. +//! * `http`: low-dependency HTTP abstractions' *front-end*. +//! +//! # Users +//! +//! * `wasm32-wasip*` integration crate `leptos_wasi` is using this +//! crate under the hood. + use super::Res; use crate::error::{ - NoCustomError, ServerFnError, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER + ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER }; use bytes::Bytes; -use futures::Stream; +use futures::{Stream, TryStreamExt}; use http::{header, HeaderValue, Response, StatusCode}; +use throw_error::Error; use std::{ - fmt::{Debug, Display}, pin::Pin, str::FromStr + fmt::{Debug, Display}, + pin::Pin, + str::FromStr, }; /// The Body of a Response whose *execution model* can be /// customised using the variants. -pub enum Body { +pub enum Body { /// The response body will be written synchronously. Sync(Bytes), - + /// The response body will be written asynchronously, /// this execution model is also known as /// "streaming". - Async(Pin>> + 'static>>), + Async( + Pin< + Box< + dyn Stream> + + Send + 'static, + >, + >, + ), } -impl From for Body { +impl From for Body { fn from(value: String) -> Self { Body::Sync(Bytes::from(value)) } } -impl Res for Response> +impl Res for Response where CustErr: Send + Sync + Debug + FromStr + Display + 'static, { @@ -65,7 +89,11 @@ where builder .status(200) .header(http::header::CONTENT_TYPE, content_type) - .body(Body::Async(Box::pin(data))) + .body( + Body::Async( + Box::pin(data.map_err(ServerFnErrorErr::from).map_err(Error::from)) + ) + ) .map_err(|e| ServerFnError::Response(e.to_string())) } diff --git a/server_fn/src/response/mod.rs b/server_fn/src/response/mod.rs index d62a85ea52..6a0f60bace 100644 --- a/server_fn/src/response/mod.rs +++ b/server_fn/src/response/mod.rs @@ -4,15 +4,14 @@ pub mod actix; /// Response types for the browser. #[cfg(feature = "browser")] pub mod browser; +#[cfg(feature = "generic")] +pub mod generic; /// Response types for Axum. #[cfg(feature = "axum-no-default")] pub mod http; /// Response types for [`reqwest`]. #[cfg(feature = "reqwest")] pub mod reqwest; -/// Generic request type -#[cfg(feature = "generic")] -pub mod generic; use crate::error::ServerFnError; use bytes::Bytes; From 98ec388a645674c49abf7434e8e634346025247d Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sat, 5 Oct 2024 12:37:38 +0200 Subject: [PATCH 04/23] feat(integrations/wasi): make WASI a first-class citizen of leptos server-side Signed-off-by: Enzo "raskyld" Nocera --- Cargo.toml | 1 + integrations/wasi/Cargo.toml | 14 +- integrations/wasi/src/handler.rs | 460 +++++++++++++++++++++++++++++ integrations/wasi/src/lib.rs | 122 ++++---- integrations/wasi/src/request.rs | 110 +++++++ integrations/wasi/src/response.rs | 137 +++++++++ integrations/wasi/src/server_fn.rs | 16 - integrations/wasi/src/utils.rs | 44 +++ 8 files changed, 827 insertions(+), 77 deletions(-) create mode 100644 integrations/wasi/src/handler.rs create mode 100644 integrations/wasi/src/request.rs create mode 100644 integrations/wasi/src/response.rs delete mode 100644 integrations/wasi/src/server_fn.rs create mode 100644 integrations/wasi/src/utils.rs diff --git a/Cargo.toml b/Cargo.toml index 2647d15a07..7ad80314f5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,6 +30,7 @@ members = [ "integrations/actix", "integrations/axum", "integrations/utils", + "integrations/wasi", # libraries "meta", diff --git a/integrations/wasi/Cargo.toml b/integrations/wasi/Cargo.toml index c7d001fb9f..0fafd9a249 100644 --- a/integrations/wasi/Cargo.toml +++ b/integrations/wasi/Cargo.toml @@ -10,20 +10,24 @@ edition.workspace = true [dependencies] any_spawner = { workspace = true, features = ["futures-executor"] } +throw_error = { workspace = true } hydration_context = { workspace = true } futures = "0.3.30" leptos = { workspace = true, features = ["nonce", "ssr"] } leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } +leptos_macro = { workspace = true, features = ["wasi"] } leptos_integration_utils = { workspace = true } -once_cell = "1" -serde_json = "1.0" +server_fn = { workspace = true, features = ["wasi"] } wit-bindgen = "0.32.0" http = "1.1.0" parking_lot = "0.12.3" +bytes = "1.7.2" +routefinder = "0.5.4" +mime_guess = "2.0" + +[features] +islands-router = [] [package.metadata.docs.rs] rustdoc-args = ["--generate-link-to-definition"] - -[package.metadata.cargo-all-features] -denylist = ["tracing"] diff --git a/integrations/wasi/src/handler.rs b/integrations/wasi/src/handler.rs new file mode 100644 index 0000000000..2374485db4 --- /dev/null +++ b/integrations/wasi/src/handler.rs @@ -0,0 +1,460 @@ +#![forbid(unsafe_code)] + +use std::sync::Arc; + +use bytes::Bytes; +use futures::{stream::{self, once}, StreamExt}; +use http::{request::Parts, HeaderValue, Uri}; +use hydration_context::SsrSharedContext; +use leptos::{ + prelude::{provide_context, Owner, ScopedFuture}, server_fn::{ + codec::Encoding, + http_export::Request, + response::generic::Body as ServerFnBody, + ServerFn, ServerFnTraitObj, + }, IntoView +}; +use leptos_integration_utils::{ExtendResponse, PinnedStream}; +use leptos_meta::ServerMetaContext; +use leptos_router::{ + components::provide_server_redirect, location::RequestUrl, PathSegment, RouteList, RouteListing, SsrMode +}; +use mime_guess::MimeGuess; +use routefinder::Router; +use server_fn::middleware::Service; +use throw_error::Error; + +use crate::{ + bindings::wasi::http::types::{IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam}, response::{Body, Response, ResponseOptions}, utils::redirect, CHUNK_BYTE_SIZE, +}; + +/// Handle routing, static file serving and response tx using the low-level +/// `wasi:http` APIs. +/// +/// ## Usage +/// +/// Please, note that the handler expect to be run with a local Executor initiated. +/// +/// ``` +/// use leptos_wasi::prelude::Handler; +/// +/// let conf = get_configuration(None).unwrap(); +/// let leptos_options = conf.leptos_options; +/// +/// Handler::build(request, response_out) +/// .expect("could not create handler") +/// // Those two functions should be called first because they can +/// // *shortcut* the handler, see "Performance Considerations". +/// +/// // Any HTTP request prefixed with `/pkg` will call the passed +/// // `serve_static_files` function to deliver static files. +/// .static_files_handler("/pkg", serve_static_files) +/// .with_server_fn::() +/// // Fetch all available routes from your App. +/// .generate_routes(App) +/// // Actually process the request and write the response. +/// .handle_with_context(move || shell(leptos_options.clone()), || {}).await.expect("could not handle the request"); +/// ``` +/// +/// ## Performance Considerations +/// +/// This handler is optimised for the special case of WASI Components being spawned +/// on a per-request basis. That is, the lifetime of the component is bound to the +/// one of the request, so we don't do any fancy pre-setup: it means +/// **your Server-Side will always be cold-started**. +/// +/// While it could have a bad impact on the performance of your app, please, know +/// that there is a *shotcut* mechanism implemented that allows the [`Handler`] +/// to shortcut the whole HTTP Rendering and Reactivity logic to directly jump to +/// writting the response in those case: +/// +/// * The user request a static-file, then, calling [`Handler::static_files_handler`] +/// will *shortcut* the handler and all future calls are ignored to reach +/// [`Handler::handle_with_context`] *almost* instantly. +/// * The user reach a server function, then, calling [`Handler::with_server_fn`] +/// will check if the request's path matches the one from the passed server functions, +/// if so, *shortcut* the handler. +/// +/// This implementation ensures that, even though your component is cold-started +/// on each request, the performance are good. Please, note that this approach is +/// directly enabled by the fact WASI Components have under-millisecond start-up +/// times! It wouldn't be practical to do that with traditional container-based solutions. +/// +/// ## Limitations +/// +/// [`SsrMode::Static`] is not implemented yet, having one in your `` +/// will cause [`Handler::handle_with_context`] to panic! +pub struct Handler { + req: Request, + res_out: ResponseOutparam, + + // *shortcut* if any is set + server_fn: Option, http::Response>>, + preset_res: Option, + should_404: bool, + + // built using the user-defined app_fn + ssr_router: Router, +} + +impl Handler { + /// Wraps the WASI resources to handle the request. + /// Could fail if the [`IncomingRequest`] cannot be converted to + /// a [`http:Request`]. + pub fn build( + req: IncomingRequest, + res_out: ResponseOutparam, + ) -> Result { + Ok(Self { + req: req.try_into()?, + res_out, + server_fn: None, + preset_res: None, + ssr_router: Router::new(), + should_404: false, + }) + } + + // Test whether we are ready to send a response to shortcut some + // code and provide a fast-path. + fn shortcut(&self) -> bool { + self.server_fn.is_some() || self.preset_res.is_some() || self.should_404 + } + + /// Tests if the request path matches the bound server function + /// and *shortcut* the [`Handler`] to quickly reach + /// the call to [`Handler::handle_with_context`]. + pub fn with_server_fn(mut self) -> Self + where + T: ServerFn< + ServerRequest = Request, + ServerResponse = http::Response, + > + 'static, + { + if self.shortcut() { + return self; + } + + if self.req.method() == T::InputEncoding::METHOD + && self.req.uri().path() == T::PATH + { + self.server_fn = Some(ServerFnTraitObj::new( + T::PATH, + T::InputEncoding::METHOD, + |request| Box::pin(T::run_on_server(request)), + T::middlewares, + )); + } + + self + } + + /// If the request is prefixed with `prefix` [`Uri`], then + /// the handler will call the passed `handler` with the Uri trimmed of + /// the prefix. If the closure returns + /// None, the response will be 404, otherwise, the returned [`Body`] + /// will be served as-if. + /// + /// This function, when matching, *shortcut* the [`Handler`] to quickly reach + /// the call to [`Handler::handle_with_context`]. + pub fn static_files_handler( + mut self, + prefix: T, + handler: impl Fn(String) -> Option + 'static + Send + Clone, + ) -> Self + where + T: TryInto, + >::Error: std::error::Error, + { + if self.shortcut() { + return self; + } + + if let Some(trimmed_url) = self.req.uri().path().strip_prefix(prefix.try_into().expect("you passed an invalid Uri").path()) { + match handler(trimmed_url.to_string()) { + None => self.should_404 = true, + Some(body) => { + let mut res = http::Response::new(body); + let mime = MimeGuess::from_path(trimmed_url); + + res + .headers_mut() + .insert( + http::header::CONTENT_TYPE, + HeaderValue::from_str( + mime.first_or_octet_stream().as_ref(), + ).expect("internal error: could not parse MIME type") + ); + + self.preset_res = Some(Response(res)); + }, + } + } + + self + } + + /// This mocks a request to the `app_fn` component to extract your + /// ``'s ``. + pub fn generate_routes( + self, + app_fn: impl Fn() -> IV + 'static + Send + Clone, + ) -> Self + where + IV: IntoView + 'static, + { + self.generate_routes_with_exclusions_and_context(app_fn, None, || {}) + } + + /// This mocks a request to the `app_fn` component to extract your + /// ``'s ``. + /// + /// You can pass an `additional_context` to [`provide_context`] to the + /// application. + pub fn generate_routes_with_context( + self, + app_fn: impl Fn() -> IV + 'static + Send + Clone, + additional_context: impl Fn() + 'static + Send + Clone, + ) -> Self + where + IV: IntoView + 'static, + { + self.generate_routes_with_exclusions_and_context( + app_fn, + None, + additional_context, + ) + } + + /// This mocks a request to the `app_fn` component to extract your + /// ``'s ``. + /// + /// You can pass an `additional_context` to [`provide_context`] to the + /// application. + /// + /// You can pass a list of `excluded_routes` to avoid generating them. + pub fn generate_routes_with_exclusions_and_context( + mut self, + app_fn: impl Fn() -> IV + 'static + Send + Clone, + excluded_routes: Option>, + additional_context: impl Fn() + 'static + Send + Clone, + ) -> Self + where + IV: IntoView + 'static, + { + // If we matched a server function, we do not need to go through + // all of that. + if self.shortcut() { + return self; + } + + if !self.ssr_router.is_empty() { + panic!("generate_routes was called twice"); + } + + let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new()))); + let (mock_meta, _) = ServerMetaContext::new(); + let routes = owner + .with(|| { + // as we are generating the app to extract + // the , we want to mock the root path. + provide_context(RequestUrl::new("")); + provide_context(ResponseOptions::default()); + provide_context(http::uri::Parts::default()); + provide_context(mock_meta); + additional_context(); + RouteList::generate(&app_fn) + }) + .unwrap_or_default() + .into_inner() + .into_iter() + .map(|rt| (rt.path().to_rf_str_representation(), rt)) + .filter(|route| { + if let Some(excluded_routes) = &excluded_routes { + !excluded_routes.iter().any(|ex_path| *ex_path == route.0) + } else { + true + } + }); + + for (path, route_listing) in routes { + self.ssr_router + .add(path, route_listing) + .expect("internal error: impossible to parse a RouteListing"); + } + + self + } + + /// Consumes the [`Handler`] to actually perform all the request handling + /// logic. + /// + /// You can pass an `additional_context` to [`provide_context`] to the + /// application. + pub async fn handle_with_context( + self, + app: impl Fn() -> IV + 'static + Send + Clone, + additional_context: impl Fn() + 'static + Clone + Send, + ) -> Result<(), Error> + where + IV: IntoView + 'static, + { + let path = self.req.uri().path().to_string(); + let best_match = self.ssr_router.best_match(&path); + let (parts, body) = self.req.into_parts(); + let context_parts = parts.clone(); + let req = Request::from_parts(parts, body); + + let owner = Owner::new(); + let response = owner.with(|| { + ScopedFuture::new(async move { + let res_opts = ResponseOptions::default(); + let response: Option = if self.should_404 { + None + } else if self.preset_res.is_some() { + self.preset_res + } else if let Some(mut sfn) = self.server_fn { + provide_contexts(additional_context, context_parts, res_opts.clone()); + Some(sfn.run(req).await.into()) + } else if let Some(best_match) = best_match { + let listing = best_match.handler(); + let (meta_context, meta_output) = ServerMetaContext::new(); + + let add_ctx = additional_context.clone(); + let additional_context = { + let res_opts = res_opts.clone(); + let meta_ctx = meta_context.clone(); + move || { + provide_contexts(add_ctx, context_parts, res_opts); + provide_context(meta_ctx); + } + }; + + Some(Response::from_app( + app, + meta_output, + additional_context, + res_opts.clone(), + match listing.mode() { + SsrMode::Async => |app, chunks| { + Box::pin(async move { + let app = if cfg!(feature = "islands-router") { + app.to_html_stream_in_order_branching() + } else { + app.to_html_stream_in_order() + }; + let app = app.collect::().await; + let chunks = chunks(); + Box::pin(once(async move { app }).chain(chunks)) as PinnedStream + }) + }, + SsrMode::InOrder => |app, chunks| { + Box::pin(async move { + let app = if cfg!(feature = "islands-router") { + app.to_html_stream_in_order_branching() + } else { + app.to_html_stream_in_order() + }; + Box::pin(app.chain(chunks())) as PinnedStream + }) + }, + SsrMode::PartiallyBlocked | SsrMode::OutOfOrder => |app, chunks| { + Box::pin(async move { + let app = if cfg!(feature = "islands-router") { + app.to_html_stream_out_of_order_branching() + } else { + app.to_html_stream_out_of_order() + }; + Box::pin(app.chain(chunks())) as PinnedStream + }) + }, + SsrMode::Static(_) => panic!("SsrMode::Static routes are not supported yet!") + } + ).await) + } else { + None + }; + + response.map(|mut req| { + req.extend_response(&res_opts); + req + }) + }) + }).await; + + let response = response.unwrap_or_else(|| { + let body = Bytes::from("404 not found"); + let mut res = http::Response::new(Body::Sync(body)); + *res.status_mut() = http::StatusCode::NOT_FOUND; + Response(res) + }); + + let headers = response.headers()?; + let wasi_res = OutgoingResponse::new(headers); + + wasi_res.set_status_code(response.0.status().as_u16()).expect("invalid http status code was returned"); + let body = wasi_res.body().expect("unable to take response body"); + ResponseOutparam::set(self.res_out, Ok(wasi_res)); + + let output_stream = body.write().expect("unable to open writable stream on body"); + let mut input_stream = match response.0.into_body() { + Body::Sync(buf) => Box::pin(stream::once(async { Ok(buf) })), + Body::Async(stream) => stream, + }; + + while let Some(buf) = input_stream.next().await { + let buf = buf?; + let chunks = buf.chunks(CHUNK_BYTE_SIZE.try_into().unwrap()); + for chunk in chunks { + // TODO: better error handling there. + output_stream.blocking_write_and_flush(chunk)?; + } + } + + drop(output_stream); + OutgoingBody::finish(body, None)?; + + Ok(()) + } +} + +fn provide_contexts( + additional_context: impl Fn() + 'static + Clone + Send, + context_parts: Parts, + res_opts: ResponseOptions, +) { + additional_context(); + provide_context(RequestUrl::new(context_parts.uri.path())); + provide_context(context_parts); + provide_context(res_opts); + provide_server_redirect(redirect); +} + +trait RouterPathRepresentation { + fn to_rf_str_representation(&self) -> String; +} + +impl RouterPathRepresentation for &[PathSegment] { + fn to_rf_str_representation(&self) -> String { + let mut path = String::new(); + for segment in self.iter() { + // TODO trailing slash handling + let raw = segment.as_raw_str(); + if !raw.is_empty() && !raw.starts_with('/') { + path.push('/'); + } + match segment { + PathSegment::Static(s) => path.push_str(s), + PathSegment::Param(s) => { + path.push(':'); + path.push_str(s); + } + PathSegment::Splat(_) => { + path.push('*'); + } + PathSegment::Unit => {} + } + } + path + } +} diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs index 4912d380a7..1a5af065a6 100644 --- a/integrations/wasi/src/lib.rs +++ b/integrations/wasi/src/lib.rs @@ -1,9 +1,57 @@ -use std::sync::Arc; - -use bindings::wasi::http::types::{Fields, IncomingRequest, OutgoingResponse}; -use http::{HeaderMap, StatusCode, HeaderName, HeaderValue}; -use hydration_context::PinnedStream; -use parking_lot::RwLock; +//! A first-party support of the `wasm32-wasip1` target for the **Server-Side** +//! of Leptos using the [`wasi:http`][wasi-http] proposal. +//! +//! [wasi-http]: https://github.com/WebAssembly/wasi-http +//! +//! # `Handler` +//! +//! The [`prelude::Handler`] is the main abstraction you will use. +//! +//! It expects being run in the context of a Future Executor `Task`, +//! since WASI is, at the moment, a single-threaded environment, +//! we provide a simple abstraction in the form of [`leptos::spawn::Executor`] +//! that you can leverage to use this crate. +//! +//! ``` +//! use leptos_wasi::{bindings::exports::wasi::http::incoming_handler::Guest, prelude::{IncomingRequest, ResponseOutparam}}; +//! +//! struct LeptosServer; +//! +//! // NB(raskyld): for now, the types to use for the HTTP handlers are the one from +//! // the `leptos_wasi` crate, not the one generated in your crate. +//! impl Guest for LeptosServer { +//! fn handle(request: IncomingRequest, response_out: ResponseOutparam) { +//! // Initiate a single-threaded [`Future`] Executor so we can run the +//! // rendering system and take advantage of bodies streaming. +//! Executor::init_futures_local_executor().expect("cannot init future executor"); +//! Executor::spawn(async { +//! // declare an async function called `handle_request` and +//! // use the Handler in this function. +//! handle_request(request, response_out).await; +//! }); +//! Executor::run(); +//! } +//! } +//! ``` +//! +//! # WASI Bindings +//! +//! You are free to use any WIT imports and export any WIT exports but at the moment, +//! when interacting with this crate, you must use the types that you can find in +//! this crate [`bindings`]. +//! +//! You then need to export your implementation using: +//! +//! ``` +//! export!(LeptosServer with_types_in leptos_wasi::bindings); +//! ``` +//! +//! If you want to use your own bindings for `wasi:http`, +//! then you need to implement `From` traits +//! to convert your own bindings into the one in [`bindings`]. +//! Please, note that it will likely implies doing `unsafe` +//! operations to wrap the resource's `handle() -> u64` in +//! another type. pub mod bindings { wit_bindgen::generate!({ @@ -14,57 +62,19 @@ pub mod bindings { }); } -pub mod server_fn; - -pub struct WasiRequest(pub IncomingRequest); - -pub struct WasiResponse { - fields: Fields, - resp: OutgoingResponse, - - /// Optional stream to consume to produce the response, - /// the tachys crate seems to produce String stream so we use - /// the same here. If it is set, the stream is consumed and the - /// chunks are appended to the body of resp. - stream: Option>, -} - -/// This struct lets you define headers and override the status of the Response from an Element or a Server Function -/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. -#[derive(Debug, Clone, Default)] -pub struct ResponseParts { - pub headers: HeaderMap, - pub status: Option, -} - -/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies. -#[derive(Debug, Clone, Default)] -pub struct ResponseOptions(Arc>); +pub mod request; +pub mod handler; +pub mod response; +pub mod utils; -impl ResponseOptions { - /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`. - pub fn overwrite(&self, parts: ResponseParts) { - let mut writable = self.0.write(); - *writable = parts - } - /// Set the status of the returned Response. - pub fn set_status(&self, status: StatusCode) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.status = Some(status); - } - /// Insert a header, overwriting any previous value with the same key. - pub fn insert_header(&self, key: HeaderName, value: HeaderValue) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.headers.insert(key, value); - } - /// Append a header, leaving any header with the same key intact. - pub fn append_header(&self, key: HeaderName, value: HeaderValue) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.headers.append(key, value); - } +pub mod prelude { + pub use crate::utils::redirect; + pub use crate::handler::Handler; + pub use crate::bindings::exports::wasi::http::incoming_handler::{IncomingRequest, ResponseOutparam}; + pub use crate::response::Body; } +/// When working with streams, this crate will try to chunk bytes with +/// this size. +const CHUNK_BYTE_SIZE: u64 = 64; diff --git a/integrations/wasi/src/request.rs b/integrations/wasi/src/request.rs new file mode 100644 index 0000000000..c85c5fc9b3 --- /dev/null +++ b/integrations/wasi/src/request.rs @@ -0,0 +1,110 @@ +use bytes::Bytes; +use http::{uri::Parts, Uri}; +use throw_error::Error; + +use crate::{ + bindings::wasi::{ + http::types::{IncomingBody, IncomingRequest, Method, Scheme}, + io::streams::StreamError, + }, + CHUNK_BYTE_SIZE, +}; + +impl TryFrom for http::Request { + type Error = Error; + + fn try_from(req: IncomingRequest) -> Result { + let mut builder = http::Request::builder(); + let req_method = req.method(); + let headers = req.headers(); + + for (header_name, header_value) in headers.entries() { + builder = builder.header(header_name, header_value); + } + + drop(headers); + + // NB(raskyld): consume could fail if, for some reason the caller + // manage to recreate an IncomingRequest backed by the same underlying + // resource handle (need to dig more to see if that's possible) + let incoming_body = req + .consume().expect("could not consume body"); + + let body_stream = incoming_body.stream().expect("could not create a stream from body"); + + let mut body_bytes = + Vec::::with_capacity(CHUNK_BYTE_SIZE.try_into().unwrap()); + + loop { + match body_stream.blocking_read(CHUNK_BYTE_SIZE) { + Err(StreamError::Closed) => break, + Err(StreamError::LastOperationFailed(err)) => { + return Err(StreamError::LastOperationFailed(err).into()) + } + Ok(data) => { + body_bytes.extend(data); + } + } + } + + let mut uri_parts = Parts::default(); + + uri_parts.scheme = req.scheme().map(http::uri::Scheme::from); + uri_parts.authority = req + .authority() + .map(|aut| { + http::uri::Authority::from_maybe_shared(aut.into_bytes()) + }) + .transpose() + .map_err(|err| Error::from(err))?; + uri_parts.path_and_query = req + .path_with_query() + .map(|paq| { + http::uri::PathAndQuery::from_maybe_shared(paq.into_bytes()) + }) + .transpose() + .map_err(|err| Error::from(err))?; + + drop(body_stream); + IncomingBody::finish(incoming_body); + builder + .method(req_method) + .uri( + Uri::from_parts(uri_parts) + .map_err(|err| Error::from(err))?, + ) + .body(Bytes::from(body_bytes)) + .map_err(|err| Error::from(err)) + } +} + +impl TryFrom for http::Method { + type Error = http::method::InvalidMethod; + + fn try_from(value: Method) -> Result { + match value { + Method::Connect => Ok(http::Method::CONNECT), + Method::Delete => Ok(http::Method::DELETE), + Method::Get => Ok(http::Method::GET), + Method::Head => Ok(http::Method::HEAD), + Method::Options => Ok(http::Method::OPTIONS), + Method::Patch => Ok(http::Method::PATCH), + Method::Post => Ok(http::Method::POST), + Method::Put => Ok(http::Method::PUT), + Method::Trace => Ok(http::Method::TRACE), + Method::Other(mtd) => http::Method::from_bytes(mtd.as_bytes()), + } + } +} + +impl From for http::uri::Scheme { + fn from(value: Scheme) -> Self { + match value { + Scheme::Http => http::uri::Scheme::HTTP, + Scheme::Https => http::uri::Scheme::HTTPS, + Scheme::Other(oth) => { + http::uri::Scheme::try_from(oth.as_bytes()).unwrap() + } + } + } +} diff --git a/integrations/wasi/src/response.rs b/integrations/wasi/src/response.rs new file mode 100644 index 0000000000..dc5f1374c5 --- /dev/null +++ b/integrations/wasi/src/response.rs @@ -0,0 +1,137 @@ +use std::{pin::Pin, sync::Arc}; + +use bytes::Bytes; +use futures::{Stream, StreamExt}; +use http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; +use leptos_integration_utils::ExtendResponse; +use parking_lot::RwLock; + +use server_fn::response::generic::Body as ServerFnBody; +use throw_error::Error; + +use crate::bindings::wasi::http::types::Headers; + +/// This crate uses platform-agnostic [`http::Response`] +/// with a custom [`Body`] and convert them under the hood to +/// WASI native types. +/// +/// It supports both [`Body::Sync`] and [`Body::Async`], +/// allowing you to choose between synchronous response +/// (i.e. sending the whole response) and asynchronous response +/// (i.e. streaming the response). +pub struct Response(pub http::Response); + +impl Response { + pub fn headers(&self) -> Result { + let headers = Headers::new(); + for (name, value) in self.0.headers() { + headers.append(&name.to_string(), &Vec::from(value.as_bytes()))?; + } + Ok(headers) + } +} + +impl From> for Response + where T: Into, +{ + fn from(value: http::Response) -> Self { + Response(value.map(Into::into)) + } +} + +pub enum Body { + /// The response body will be written synchronously. + Sync(Bytes), + + /// The response body will be written asynchronously, + /// this execution model is also known as + /// "streaming". + Async( + Pin< + Box< + dyn Stream> + + Send + 'static, + >, + >, + ), +} + +impl From for Body { + fn from(value: ServerFnBody) -> Self { + match value { + ServerFnBody::Sync(data) => Body::Sync(data), + ServerFnBody::Async(stream) => Body::Async(stream), + } + } +} + + +/// This struct lets you define headers and override the status of the Response from an Element or a Server Function +/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. +#[derive(Debug, Clone, Default)] +pub struct ResponseParts { + pub headers: HeaderMap, + pub status: Option, +} + +/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies. +#[derive(Debug, Clone, Default)] +pub struct ResponseOptions(Arc>); + +impl ResponseOptions { + /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`. + pub fn overwrite(&self, parts: ResponseParts) { + let mut writable = self.0.write(); + *writable = parts + } + /// Set the status of the returned Response. + pub fn set_status(&self, status: StatusCode) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.status = Some(status); + } + /// Insert a header, overwriting any previous value with the same key. + pub fn insert_header(&self, key: HeaderName, value: HeaderValue) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.headers.insert(key, value); + } + /// Append a header, leaving any header with the same key intact. + pub fn append_header(&self, key: HeaderName, value: HeaderValue) { + let mut writeable = self.0.write(); + let res_parts = &mut *writeable; + res_parts.headers.append(key, value); + } +} + +impl ExtendResponse for Response { + type ResponseOptions = ResponseOptions; + + fn from_stream(stream: impl Stream + Send + 'static) + -> Self { + let stream = stream + .map(|data| { + Result::::Ok(Bytes::from(data)) + }); + + Response(http::Response::new(Body::Async(Box::pin(stream)))) + } + + fn extend_response(&mut self, opt: &Self::ResponseOptions) { + let mut opt = opt.0.write(); + if let Some(status_code) = opt.status { + *self.0.status_mut() = status_code; + } + self.0.headers_mut().extend(std::mem::take(&mut opt.headers)); + } + + fn set_default_content_type(&mut self, content_type: &str) { + let headers = self.0.headers_mut(); + if !headers.contains_key(http::header::CONTENT_TYPE) { + headers.insert( + http::header::CONTENT_TYPE, + HeaderValue::from_str(content_type).unwrap(), + ); + } + } +} diff --git a/integrations/wasi/src/server_fn.rs b/integrations/wasi/src/server_fn.rs deleted file mode 100644 index b28db78e35..0000000000 --- a/integrations/wasi/src/server_fn.rs +++ /dev/null @@ -1,16 +0,0 @@ -#![forbid(unsafe_code)] - -//! A simplification of Leptos Server Functions inventory. -//! Since it's not possible in Rust to use most crates relying on -//! linker sections when targeting `wasm32` (e.g. `inventory` crate), -//! and since our component's lifetime is bound to the one of the -//! incoming request, we can simplificate the codebase a lot. - -use leptos::server_fn::ServerFnTraitObj; - -use crate::{WasiRequest, WasiResponse}; - -pub enum Matcher { - Found(ServerFnTraitObj), - NotFound, -} diff --git a/integrations/wasi/src/utils.rs b/integrations/wasi/src/utils.rs new file mode 100644 index 0000000000..22a61d8604 --- /dev/null +++ b/integrations/wasi/src/utils.rs @@ -0,0 +1,44 @@ +use http::{header::{self, ACCEPT}, request::Parts, HeaderName, HeaderValue, StatusCode}; +use leptos::prelude::use_context; +use server_fn::redirect::REDIRECT_HEADER; + +use crate::response::ResponseOptions; + +// Allow the user to perform redirection easily. +pub fn redirect(path: &str) { + if let (Some(req), Some(res)) = + (use_context::(), use_context::()) + { + // insert the Location header in any case + res.insert_header( + header::LOCATION, + header::HeaderValue::from_str(path) + .expect("Failed to create HeaderValue"), + ); + + let accepts_html = req + .headers + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + if accepts_html { + // if the request accepts text/html, it's a plain form request and needs + // to have the 302 code set + res.set_status(StatusCode::FOUND); + } else { + // otherwise, we sent it from the server fn client and actually don't want + // to set a real redirect, as this will break the ability to return data + // instead, set the REDIRECT_HEADER to indicate that the client should redirect + res.insert_header( + HeaderName::from_static(REDIRECT_HEADER), + HeaderValue::from_str("").unwrap(), + ); + } + } else { + eprintln!( + "Couldn't retrieve either Parts or ResponseOptions while \ + trying to redirect()." + ); + } +} From 3d76ba40e199ffac9dbf20df952da2042beda4af Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sat, 5 Oct 2024 12:38:55 +0200 Subject: [PATCH 05/23] WIP: chore(any_spawner): make the futures::Executor runable Signed-off-by: Enzo "raskyld" Nocera --- any_spawner/src/lib.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/any_spawner/src/lib.rs b/any_spawner/src/lib.rs index a03196a23b..4df5b2e47e 100644 --- a/any_spawner/src/lib.rs +++ b/any_spawner/src/lib.rs @@ -110,6 +110,27 @@ impl Executor { } } + /// Run the [`Executor`]. + #[track_caller] + pub fn run() { + if let Some(run) = RUN.get() { + run(); + } else { + #[cfg(all(debug_assertions, feature = "tracing"))] + tracing::error!( + "At {}, tried to run an executor with Executor::run() \ + before the Executor had been set.", + std::panic::Location::caller() + ); + #[cfg(all(debug_assertions, not(feature = "tracing")))] + panic!( + "At {}, tried to run an executor with Executor::run() \ + before the Executor had been set.", + std::panic::Location::caller() + ); + } + } + /// Waits until the next "tick" of the current async executor. pub async fn tick() { let (tx, rx) = futures::channel::oneshot::channel(); From f0c0939cd315dbf0141f0cb4f13b1b1cbc341ca1 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sun, 6 Oct 2024 13:56:30 +0200 Subject: [PATCH 06/23] fix(server_fn): include `generic` in axum. Signed-off-by: Enzo "raskyld" Nocera --- server_fn/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index 9d2bb2c9d9..a96fc6cd60 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -80,6 +80,7 @@ pin-project-lite = "0.2.14" default = ["json"] axum-no-default = [ "ssr", + "generic", "dep:axum", "dep:hyper", "dep:http-body-util", From 3978a990b137dd4859b904c7d0a0a010d589286c Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sun, 6 Oct 2024 15:29:24 +0200 Subject: [PATCH 07/23] chore(any_spawner): some clippy suggestions I ran clippy in really annoying mode since I am still learning Rust and I want to write clean idiomatic code. I took suggestions that I thought made sense, if any maintainers think those are *too much*, I can relax those changes: * Use `core` instead of `std` to ease migration to `no_std` (https://rust-lang.github.io/rust-clippy/master/index.html#/std_instead_of_core) * Add documentation on exported types and statics * Bring some types in, with `use` * Add `#[non_exhaustive]` on types we are not sure we won't extend (https://rust-lang.github.io/rust-clippy/master/index.html#exhaustive_enums) * Add `#[inline]` to help the compiler when doing cross-crate compilation and Link-Time optimization is not enabled. (https://rust-lang.github.io/rust-clippy/master/index.html#/missing_inline_in_public_items) * Use generic types instead of anonymous `impl` params so callers can use the `::<>` turbofish syntax (https://rust-lang.github.io/rust-clippy/master/index.html#/impl_trait_in_params) Signed-off-by: Enzo "raskyld" Nocera --- any_spawner/src/lib.rs | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/any_spawner/src/lib.rs b/any_spawner/src/lib.rs index 4df5b2e47e..574d6f8c01 100644 --- a/any_spawner/src/lib.rs +++ b/any_spawner/src/lib.rs @@ -29,7 +29,9 @@ #![deny(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] -use std::{future::Future, pin::Pin, sync::OnceLock}; +use core::{future::Future, pin::Pin, panic::Location}; +use std::sync::OnceLock; +use futures::channel::oneshot; use thiserror::Error; /// A future that has been pinned. @@ -37,12 +39,18 @@ pub type PinnedFuture = Pin + Send>>; /// A future that has been pinned. pub type PinnedLocalFuture = Pin>>; +/// Handle to spawn a new [`PinnedFuture`] on the initiated [`Executor`]. static SPAWN: OnceLock)> = OnceLock::new(); + +/// Handle to spawn a new [`PinnedLocalFuture`] on the initiated [`Executor`]. +/// +/// It is useful when you have a Future that is not [`Send`]. static SPAWN_LOCAL: OnceLock)> = OnceLock::new(); static POLL_LOCAL: OnceLock = OnceLock::new(); /// Errors that can occur when using the executor. #[derive(Error, Debug)] +#[non_exhaustive] pub enum ExecutorError { /// The executor has already been set. #[error("Executor has already been set.")] @@ -62,7 +70,10 @@ impl Executor { /// # } /// ``` #[track_caller] - pub fn spawn(fut: impl Future + Send + 'static) { + #[inline] + pub fn spawn(fut: T) + where T: Future + Send + 'static + { if let Some(spawner) = SPAWN.get() { spawner(Box::pin(fut)) } else { @@ -70,13 +81,13 @@ impl Executor { tracing::error!( "At {}, tried to spawn a Future with Executor::spawn() before \ the Executor had been set.", - std::panic::Location::caller() + Location::caller() ); #[cfg(all(debug_assertions, not(feature = "tracing")))] panic!( "At {}, tried to spawn a Future with Executor::spawn() before \ the Executor had been set.", - std::panic::Location::caller() + Location::caller() ); } } @@ -91,7 +102,10 @@ impl Executor { /// # } /// ``` #[track_caller] - pub fn spawn_local(fut: impl Future + 'static) { + #[inline] + pub fn spawn_local(fut: T) + where T: Future + 'static + { if let Some(spawner) = SPAWN_LOCAL.get() { spawner(Box::pin(fut)) } else { @@ -99,19 +113,20 @@ impl Executor { tracing::error!( "At {}, tried to spawn a Future with Executor::spawn_local() \ before the Executor had been set.", - std::panic::Location::caller() + Location::caller() ); #[cfg(all(debug_assertions, not(feature = "tracing")))] panic!( "At {}, tried to spawn a Future with Executor::spawn_local() \ before the Executor had been set.", - std::panic::Location::caller() + Location::caller() ); } } /// Run the [`Executor`]. #[track_caller] + #[inline] pub fn run() { if let Some(run) = RUN.get() { run(); @@ -120,21 +135,22 @@ impl Executor { tracing::error!( "At {}, tried to run an executor with Executor::run() \ before the Executor had been set.", - std::panic::Location::caller() + Location::caller() ); #[cfg(all(debug_assertions, not(feature = "tracing")))] panic!( "At {}, tried to run an executor with Executor::run() \ before the Executor had been set.", - std::panic::Location::caller() + Location::caller() ); } } /// Waits until the next "tick" of the current async executor. + #[inline] pub async fn tick() { - let (tx, rx) = futures::channel::oneshot::channel(); - Executor::spawn(async move { + let (tx, rx) = oneshot::channel(); + Self::spawn(async move { _ = tx.send(()); }); _ = rx.await; From 071da3aa4776c5a9c47a4d760c665a2f6a1e460a Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sun, 6 Oct 2024 17:11:26 +0200 Subject: [PATCH 08/23] chore(leptos_wasi): fine-tune linter and clean-up Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/src/handler.rs | 15 +++++----- integrations/wasi/src/lib.rs | 9 +++++- integrations/wasi/src/request.rs | 50 +++++++++++++++++-------------- integrations/wasi/src/response.rs | 27 ++++++++--------- 4 files changed, 55 insertions(+), 46 deletions(-) diff --git a/integrations/wasi/src/handler.rs b/integrations/wasi/src/handler.rs index 2374485db4..fbd0785bda 100644 --- a/integrations/wasi/src/handler.rs +++ b/integrations/wasi/src/handler.rs @@ -117,7 +117,8 @@ impl Handler { // Test whether we are ready to send a response to shortcut some // code and provide a fast-path. - fn shortcut(&self) -> bool { + #[inline] + const fn shortcut(&self) -> bool { self.server_fn.is_some() || self.preset_res.is_some() || self.should_404 } @@ -270,11 +271,11 @@ impl Handler { .into_iter() .map(|rt| (rt.path().to_rf_str_representation(), rt)) .filter(|route| { - if let Some(excluded_routes) = &excluded_routes { - !excluded_routes.iter().any(|ex_path| *ex_path == route.0) - } else { - true - } + excluded_routes + .as_ref() + .map_or(true, |excluded_routes| { + !excluded_routes.iter().any(|ex_path| *ex_path == route.0) + }) }); for (path, route_listing) in routes { @@ -404,7 +405,7 @@ impl Handler { while let Some(buf) = input_stream.next().await { let buf = buf?; - let chunks = buf.chunks(CHUNK_BYTE_SIZE.try_into().unwrap()); + let chunks = buf.chunks(CHUNK_BYTE_SIZE); for chunk in chunks { // TODO: better error handling there. output_stream.blocking_write_and_flush(chunk)?; diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs index 1a5af065a6..67ce7e1e38 100644 --- a/integrations/wasi/src/lib.rs +++ b/integrations/wasi/src/lib.rs @@ -53,6 +53,12 @@ //! operations to wrap the resource's `handle() -> u64` in //! another type. +#![warn(clippy::all)] +#![warn(clippy::nursery)] +#![allow(clippy::restriction)] +#![deny(clippy::allow_attributes)] + +#[allow(warnings)] pub mod bindings { wit_bindgen::generate!({ path: "wit", @@ -67,6 +73,7 @@ pub mod handler; pub mod response; pub mod utils; +#[allow(clippy::pub_use)] pub mod prelude { pub use crate::utils::redirect; pub use crate::handler::Handler; @@ -76,5 +83,5 @@ pub mod prelude { /// When working with streams, this crate will try to chunk bytes with /// this size. -const CHUNK_BYTE_SIZE: u64 = 64; +const CHUNK_BYTE_SIZE: usize = 64; diff --git a/integrations/wasi/src/request.rs b/integrations/wasi/src/request.rs index c85c5fc9b3..457da47df5 100644 --- a/integrations/wasi/src/request.rs +++ b/integrations/wasi/src/request.rs @@ -1,5 +1,5 @@ use bytes::Bytes; -use http::{uri::Parts, Uri}; +use http::{uri::{InvalidUri, Parts}, Uri}; use throw_error::Error; use crate::{ @@ -33,10 +33,10 @@ impl TryFrom for http::Request { let body_stream = incoming_body.stream().expect("could not create a stream from body"); let mut body_bytes = - Vec::::with_capacity(CHUNK_BYTE_SIZE.try_into().unwrap()); + Vec::::with_capacity(CHUNK_BYTE_SIZE); loop { - match body_stream.blocking_read(CHUNK_BYTE_SIZE) { + match body_stream.blocking_read(CHUNK_BYTE_SIZE as u64) { Err(StreamError::Closed) => break, Err(StreamError::LastOperationFailed(err)) => { return Err(StreamError::LastOperationFailed(err).into()) @@ -49,21 +49,24 @@ impl TryFrom for http::Request { let mut uri_parts = Parts::default(); - uri_parts.scheme = req.scheme().map(http::uri::Scheme::from); + uri_parts.scheme = req + .scheme() + .map(http::uri::Scheme::try_from) + .transpose()?; uri_parts.authority = req .authority() .map(|aut| { http::uri::Authority::from_maybe_shared(aut.into_bytes()) }) .transpose() - .map_err(|err| Error::from(err))?; + .map_err(Error::from)?; uri_parts.path_and_query = req .path_with_query() .map(|paq| { http::uri::PathAndQuery::from_maybe_shared(paq.into_bytes()) }) .transpose() - .map_err(|err| Error::from(err))?; + .map_err(Error::from)?; drop(body_stream); IncomingBody::finish(incoming_body); @@ -71,10 +74,10 @@ impl TryFrom for http::Request { .method(req_method) .uri( Uri::from_parts(uri_parts) - .map_err(|err| Error::from(err))?, + .map_err(Error::from)?, ) .body(Bytes::from(body_bytes)) - .map_err(|err| Error::from(err)) + .map_err(Error::from) } } @@ -83,27 +86,28 @@ impl TryFrom for http::Method { fn try_from(value: Method) -> Result { match value { - Method::Connect => Ok(http::Method::CONNECT), - Method::Delete => Ok(http::Method::DELETE), - Method::Get => Ok(http::Method::GET), - Method::Head => Ok(http::Method::HEAD), - Method::Options => Ok(http::Method::OPTIONS), - Method::Patch => Ok(http::Method::PATCH), - Method::Post => Ok(http::Method::POST), - Method::Put => Ok(http::Method::PUT), - Method::Trace => Ok(http::Method::TRACE), - Method::Other(mtd) => http::Method::from_bytes(mtd.as_bytes()), + Method::Connect => Ok(Self::CONNECT), + Method::Delete => Ok(Self::DELETE), + Method::Get => Ok(Self::GET), + Method::Head => Ok(Self::HEAD), + Method::Options => Ok(Self::OPTIONS), + Method::Patch => Ok(Self::PATCH), + Method::Post => Ok(Self::POST), + Method::Put => Ok(Self::PUT), + Method::Trace => Ok(Self::TRACE), + Method::Other(mtd) => Self::from_bytes(mtd.as_bytes()), } } } -impl From for http::uri::Scheme { - fn from(value: Scheme) -> Self { +impl TryFrom for http::uri::Scheme { + type Error = InvalidUri; + fn try_from(value: Scheme) -> Result { match value { - Scheme::Http => http::uri::Scheme::HTTP, - Scheme::Https => http::uri::Scheme::HTTPS, + Scheme::Http => Ok(Self::HTTP), + Scheme::Https => Ok(Self::HTTPS), Scheme::Other(oth) => { - http::uri::Scheme::try_from(oth.as_bytes()).unwrap() + Self::try_from(oth.as_bytes()) } } } diff --git a/integrations/wasi/src/response.rs b/integrations/wasi/src/response.rs index dc5f1374c5..fe1856646d 100644 --- a/integrations/wasi/src/response.rs +++ b/integrations/wasi/src/response.rs @@ -35,7 +35,7 @@ impl From> for Response where T: Into, { fn from(value: http::Response) -> Self { - Response(value.map(Into::into)) + Self(value.map(Into::into)) } } @@ -59,8 +59,8 @@ pub enum Body { impl From for Body { fn from(value: ServerFnBody) -> Self { match value { - ServerFnBody::Sync(data) => Body::Sync(data), - ServerFnBody::Async(stream) => Body::Async(stream), + ServerFnBody::Sync(data) => Self::Sync(data), + ServerFnBody::Async(stream) => Self::Async(stream), } } } @@ -80,27 +80,24 @@ pub struct ResponseOptions(Arc>); impl ResponseOptions { /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`. + #[inline] pub fn overwrite(&self, parts: ResponseParts) { - let mut writable = self.0.write(); - *writable = parts + *self.0.write() = parts } /// Set the status of the returned Response. + #[inline] pub fn set_status(&self, status: StatusCode) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.status = Some(status); + self.0.write().status = Some(status); } /// Insert a header, overwriting any previous value with the same key. + #[inline] pub fn insert_header(&self, key: HeaderName, value: HeaderValue) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.headers.insert(key, value); + self.0.write().headers.insert(key, value); } /// Append a header, leaving any header with the same key intact. + #[inline] pub fn append_header(&self, key: HeaderName, value: HeaderValue) { - let mut writeable = self.0.write(); - let res_parts = &mut *writeable; - res_parts.headers.append(key, value); + self.0.write().headers.append(key, value); } } @@ -114,7 +111,7 @@ impl ExtendResponse for Response { Result::::Ok(Bytes::from(data)) }); - Response(http::Response::new(Body::Async(Box::pin(stream)))) + Self(http::Response::new(Body::Async(Box::pin(stream)))) } fn extend_response(&mut self, opt: &Self::ResponseOptions) { From afb05b0c82cd91f07e3da3116ef4dd05d9dbfe44 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sun, 6 Oct 2024 17:13:43 +0200 Subject: [PATCH 09/23] feat(leptos_wasi): better handling of server fn with form Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/src/handler.rs | 34 ++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/integrations/wasi/src/handler.rs b/integrations/wasi/src/handler.rs index fbd0785bda..5ac33fefde 100644 --- a/integrations/wasi/src/handler.rs +++ b/integrations/wasi/src/handler.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use bytes::Bytes; use futures::{stream::{self, once}, StreamExt}; -use http::{request::Parts, HeaderValue, Uri}; +use http::{header::{ACCEPT, LOCATION, REFERER}, request::Parts, HeaderValue, StatusCode, Uri}; use hydration_context::SsrSharedContext; use leptos::{ prelude::{provide_context, Owner, ScopedFuture}, server_fn::{ @@ -316,7 +316,37 @@ impl Handler { self.preset_res } else if let Some(mut sfn) = self.server_fn { provide_contexts(additional_context, context_parts, res_opts.clone()); - Some(sfn.run(req).await.into()) + + // store Accepts and Referer in case we need them for redirect (below) + let accepts_html = req + .headers() + .get(ACCEPT) + .and_then(|v| v.to_str().ok()) + .map(|v| v.contains("text/html")) + .unwrap_or(false); + let referrer = req.headers().get(REFERER).cloned(); + + let mut res = sfn.run(req).await; + + // it it accepts text/html (i.e., is a plain form post) and doesn't already have a + // Location set, then redirect to to Referer + if accepts_html { + if let Some(referrer) = referrer { + let has_location = + res.headers().get(LOCATION).is_some(); + if !has_location { + *res.status_mut() = StatusCode::FOUND; + res.headers_mut().insert(LOCATION, referrer); + } + } + } + + match res.body() { + ServerFnBody::Async(_) => println!("cant print stream"), + ServerFnBody::Sync(data) => println!("will send : {}", String::from_utf8_lossy(data)) + }; + + Some(res.into()) } else if let Some(best_match) = best_match { let listing = best_match.handler(); let (meta_context, meta_output) = ServerMetaContext::new(); From cc6f5fc7567e12a01b900dffef4f6b4cf5b19926 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sun, 6 Oct 2024 17:16:59 +0200 Subject: [PATCH 10/23] chore: cargo fmt Signed-off-by: Enzo "raskyld" Nocera --- any_spawner/src/lib.rs | 12 ++-- integrations/wasi/src/handler.rs | 111 +++++++++++++++++------------- integrations/wasi/src/lib.rs | 31 +++++---- integrations/wasi/src/request.rs | 30 ++++---- integrations/wasi/src/response.rs | 32 ++++----- integrations/wasi/src/utils.rs | 6 +- server_fn/src/request/generic.rs | 8 +-- server_fn/src/response/generic.rs | 29 +++----- server_fn_macro/src/lib.rs | 13 ++-- 9 files changed, 138 insertions(+), 134 deletions(-) diff --git a/any_spawner/src/lib.rs b/any_spawner/src/lib.rs index 574d6f8c01..1fcaee6a8e 100644 --- a/any_spawner/src/lib.rs +++ b/any_spawner/src/lib.rs @@ -29,9 +29,9 @@ #![deny(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] -use core::{future::Future, pin::Pin, panic::Location}; -use std::sync::OnceLock; +use core::{future::Future, panic::Location, pin::Pin}; use futures::channel::oneshot; +use std::sync::OnceLock; use thiserror::Error; /// A future that has been pinned. @@ -43,7 +43,7 @@ pub type PinnedLocalFuture = Pin>>; static SPAWN: OnceLock)> = OnceLock::new(); /// Handle to spawn a new [`PinnedLocalFuture`] on the initiated [`Executor`]. -/// +/// /// It is useful when you have a Future that is not [`Send`]. static SPAWN_LOCAL: OnceLock)> = OnceLock::new(); static POLL_LOCAL: OnceLock = OnceLock::new(); @@ -72,7 +72,8 @@ impl Executor { #[track_caller] #[inline] pub fn spawn(fut: T) - where T: Future + Send + 'static + where + T: Future + Send + 'static, { if let Some(spawner) = SPAWN.get() { spawner(Box::pin(fut)) @@ -104,7 +105,8 @@ impl Executor { #[track_caller] #[inline] pub fn spawn_local(fut: T) - where T: Future + 'static + where + T: Future + 'static, { if let Some(spawner) = SPAWN_LOCAL.get() { spawner(Box::pin(fut)) diff --git a/integrations/wasi/src/handler.rs b/integrations/wasi/src/handler.rs index 5ac33fefde..7b34d6f9c9 100644 --- a/integrations/wasi/src/handler.rs +++ b/integrations/wasi/src/handler.rs @@ -3,21 +3,29 @@ use std::sync::Arc; use bytes::Bytes; -use futures::{stream::{self, once}, StreamExt}; -use http::{header::{ACCEPT, LOCATION, REFERER}, request::Parts, HeaderValue, StatusCode, Uri}; +use futures::{ + stream::{self, once}, + StreamExt, +}; +use http::{ + header::{ACCEPT, LOCATION, REFERER}, + request::Parts, + HeaderValue, StatusCode, Uri, +}; use hydration_context::SsrSharedContext; use leptos::{ - prelude::{provide_context, Owner, ScopedFuture}, server_fn::{ - codec::Encoding, - http_export::Request, - response::generic::Body as ServerFnBody, - ServerFn, ServerFnTraitObj, - }, IntoView + prelude::{provide_context, Owner, ScopedFuture}, + server_fn::{ + codec::Encoding, http_export::Request, + response::generic::Body as ServerFnBody, ServerFn, ServerFnTraitObj, + }, + IntoView, }; use leptos_integration_utils::{ExtendResponse, PinnedStream}; use leptos_meta::ServerMetaContext; use leptos_router::{ - components::provide_server_redirect, location::RequestUrl, PathSegment, RouteList, RouteListing, SsrMode + components::provide_server_redirect, location::RequestUrl, PathSegment, + RouteList, RouteListing, SsrMode, }; use mime_guess::MimeGuess; use routefinder::Router; @@ -25,19 +33,24 @@ use server_fn::middleware::Service; use throw_error::Error; use crate::{ - bindings::wasi::http::types::{IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam}, response::{Body, Response, ResponseOptions}, utils::redirect, CHUNK_BYTE_SIZE, + bindings::wasi::http::types::{ + IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, + }, + response::{Body, Response, ResponseOptions}, + utils::redirect, + CHUNK_BYTE_SIZE, }; /// Handle routing, static file serving and response tx using the low-level /// `wasi:http` APIs. -/// +/// /// ## Usage -/// +/// /// Please, note that the handler expect to be run with a local Executor initiated. -/// +/// /// ``` /// use leptos_wasi::prelude::Handler; -/// +/// /// let conf = get_configuration(None).unwrap(); /// let leptos_options = conf.leptos_options; /// @@ -45,7 +58,7 @@ use crate::{ /// .expect("could not create handler") /// // Those two functions should be called first because they can /// // *shortcut* the handler, see "Performance Considerations". -/// +/// /// // Any HTTP request prefixed with `/pkg` will call the passed /// // `serve_static_files` function to deliver static files. /// .static_files_handler("/pkg", serve_static_files) @@ -55,33 +68,33 @@ use crate::{ /// // Actually process the request and write the response. /// .handle_with_context(move || shell(leptos_options.clone()), || {}).await.expect("could not handle the request"); /// ``` -/// +/// /// ## Performance Considerations -/// +/// /// This handler is optimised for the special case of WASI Components being spawned /// on a per-request basis. That is, the lifetime of the component is bound to the /// one of the request, so we don't do any fancy pre-setup: it means /// **your Server-Side will always be cold-started**. -/// +/// /// While it could have a bad impact on the performance of your app, please, know /// that there is a *shotcut* mechanism implemented that allows the [`Handler`] /// to shortcut the whole HTTP Rendering and Reactivity logic to directly jump to /// writting the response in those case: -/// +/// /// * The user request a static-file, then, calling [`Handler::static_files_handler`] /// will *shortcut* the handler and all future calls are ignored to reach /// [`Handler::handle_with_context`] *almost* instantly. /// * The user reach a server function, then, calling [`Handler::with_server_fn`] /// will check if the request's path matches the one from the passed server functions, /// if so, *shortcut* the handler. -/// +/// /// This implementation ensures that, even though your component is cold-started /// on each request, the performance are good. Please, note that this approach is /// directly enabled by the fact WASI Components have under-millisecond start-up /// times! It wouldn't be practical to do that with traditional container-based solutions. -/// +/// /// ## Limitations -/// +/// /// [`SsrMode::Static`] is not implemented yet, having one in your `` /// will cause [`Handler::handle_with_context`] to panic! pub struct Handler { @@ -89,7 +102,8 @@ pub struct Handler { res_out: ResponseOutparam, // *shortcut* if any is set - server_fn: Option, http::Response>>, + server_fn: + Option, http::Response>>, preset_res: Option, should_404: bool, @@ -155,7 +169,7 @@ impl Handler { /// the prefix. If the closure returns /// None, the response will be 404, otherwise, the returned [`Body`] /// will be served as-if. - /// + /// /// This function, when matching, *shortcut* the [`Handler`] to quickly reach /// the call to [`Handler::handle_with_context`]. pub fn static_files_handler( @@ -163,32 +177,33 @@ impl Handler { prefix: T, handler: impl Fn(String) -> Option + 'static + Send + Clone, ) -> Self - where - T: TryInto, - >::Error: std::error::Error, + where + T: TryInto, + >::Error: std::error::Error, { if self.shortcut() { return self; } - if let Some(trimmed_url) = self.req.uri().path().strip_prefix(prefix.try_into().expect("you passed an invalid Uri").path()) { + if let Some(trimmed_url) = self.req.uri().path().strip_prefix( + prefix.try_into().expect("you passed an invalid Uri").path(), + ) { match handler(trimmed_url.to_string()) { None => self.should_404 = true, Some(body) => { let mut res = http::Response::new(body); let mime = MimeGuess::from_path(trimmed_url); - res - .headers_mut() - .insert( - http::header::CONTENT_TYPE, - HeaderValue::from_str( - mime.first_or_octet_stream().as_ref(), - ).expect("internal error: could not parse MIME type") - ); + res.headers_mut().insert( + http::header::CONTENT_TYPE, + HeaderValue::from_str( + mime.first_or_octet_stream().as_ref(), + ) + .expect("internal error: could not parse MIME type"), + ); self.preset_res = Some(Response(res)); - }, + } } } @@ -209,7 +224,7 @@ impl Handler { /// This mocks a request to the `app_fn` component to extract your /// ``'s ``. - /// + /// /// You can pass an `additional_context` to [`provide_context`] to the /// application. pub fn generate_routes_with_context( @@ -229,10 +244,10 @@ impl Handler { /// This mocks a request to the `app_fn` component to extract your /// ``'s ``. - /// + /// /// You can pass an `additional_context` to [`provide_context`] to the /// application. - /// + /// /// You can pass a list of `excluded_routes` to avoid generating them. pub fn generate_routes_with_exclusions_and_context( mut self, @@ -271,10 +286,8 @@ impl Handler { .into_iter() .map(|rt| (rt.path().to_rf_str_representation(), rt)) .filter(|route| { - excluded_routes - .as_ref() - .map_or(true, |excluded_routes| { - !excluded_routes.iter().any(|ex_path| *ex_path == route.0) + excluded_routes.as_ref().map_or(true, |excluded_routes| { + !excluded_routes.iter().any(|ex_path| *ex_path == route.0) }) }); @@ -289,7 +302,7 @@ impl Handler { /// Consumes the [`Handler`] to actually perform all the request handling /// logic. - /// + /// /// You can pass an `additional_context` to [`provide_context`] to the /// application. pub async fn handle_with_context( @@ -423,11 +436,15 @@ impl Handler { let headers = response.headers()?; let wasi_res = OutgoingResponse::new(headers); - wasi_res.set_status_code(response.0.status().as_u16()).expect("invalid http status code was returned"); + wasi_res + .set_status_code(response.0.status().as_u16()) + .expect("invalid http status code was returned"); let body = wasi_res.body().expect("unable to take response body"); ResponseOutparam::set(self.res_out, Ok(wasi_res)); - let output_stream = body.write().expect("unable to open writable stream on body"); + let output_stream = body + .write() + .expect("unable to open writable stream on body"); let mut input_stream = match response.0.into_body() { Body::Sync(buf) => Box::pin(stream::once(async { Ok(buf) })), Body::Async(stream) => stream, diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs index 67ce7e1e38..731e271118 100644 --- a/integrations/wasi/src/lib.rs +++ b/integrations/wasi/src/lib.rs @@ -1,20 +1,20 @@ //! A first-party support of the `wasm32-wasip1` target for the **Server-Side** //! of Leptos using the [`wasi:http`][wasi-http] proposal. -//! +//! //! [wasi-http]: https://github.com/WebAssembly/wasi-http -//! +//! //! # `Handler` -//! +//! //! The [`prelude::Handler`] is the main abstraction you will use. -//! +//! //! It expects being run in the context of a Future Executor `Task`, //! since WASI is, at the moment, a single-threaded environment, //! we provide a simple abstraction in the form of [`leptos::spawn::Executor`] //! that you can leverage to use this crate. -//! +//! //! ``` //! use leptos_wasi::{bindings::exports::wasi::http::incoming_handler::Guest, prelude::{IncomingRequest, ResponseOutparam}}; -//! +//! //! struct LeptosServer; //! //! // NB(raskyld): for now, the types to use for the HTTP handlers are the one from @@ -33,19 +33,19 @@ //! } //! } //! ``` -//! +//! //! # WASI Bindings -//! +//! //! You are free to use any WIT imports and export any WIT exports but at the moment, //! when interacting with this crate, you must use the types that you can find in //! this crate [`bindings`]. -//! +//! //! You then need to export your implementation using: -//! +//! //! ``` //! export!(LeptosServer with_types_in leptos_wasi::bindings); //! ``` -//! +//! //! If you want to use your own bindings for `wasi:http`, //! then you need to implement `From` traits //! to convert your own bindings into the one in [`bindings`]. @@ -68,20 +68,21 @@ pub mod bindings { }); } -pub mod request; pub mod handler; +pub mod request; pub mod response; pub mod utils; #[allow(clippy::pub_use)] pub mod prelude { - pub use crate::utils::redirect; + pub use crate::bindings::exports::wasi::http::incoming_handler::{ + IncomingRequest, ResponseOutparam, + }; pub use crate::handler::Handler; - pub use crate::bindings::exports::wasi::http::incoming_handler::{IncomingRequest, ResponseOutparam}; pub use crate::response::Body; + pub use crate::utils::redirect; } /// When working with streams, this crate will try to chunk bytes with /// this size. const CHUNK_BYTE_SIZE: usize = 64; - diff --git a/integrations/wasi/src/request.rs b/integrations/wasi/src/request.rs index 457da47df5..06ae28bcdd 100644 --- a/integrations/wasi/src/request.rs +++ b/integrations/wasi/src/request.rs @@ -1,5 +1,8 @@ use bytes::Bytes; -use http::{uri::{InvalidUri, Parts}, Uri}; +use http::{ + uri::{InvalidUri, Parts}, + Uri, +}; use throw_error::Error; use crate::{ @@ -27,13 +30,13 @@ impl TryFrom for http::Request { // NB(raskyld): consume could fail if, for some reason the caller // manage to recreate an IncomingRequest backed by the same underlying // resource handle (need to dig more to see if that's possible) - let incoming_body = req - .consume().expect("could not consume body"); + let incoming_body = req.consume().expect("could not consume body"); - let body_stream = incoming_body.stream().expect("could not create a stream from body"); + let body_stream = incoming_body + .stream() + .expect("could not create a stream from body"); - let mut body_bytes = - Vec::::with_capacity(CHUNK_BYTE_SIZE); + let mut body_bytes = Vec::::with_capacity(CHUNK_BYTE_SIZE); loop { match body_stream.blocking_read(CHUNK_BYTE_SIZE as u64) { @@ -49,10 +52,8 @@ impl TryFrom for http::Request { let mut uri_parts = Parts::default(); - uri_parts.scheme = req - .scheme() - .map(http::uri::Scheme::try_from) - .transpose()?; + uri_parts.scheme = + req.scheme().map(http::uri::Scheme::try_from).transpose()?; uri_parts.authority = req .authority() .map(|aut| { @@ -72,10 +73,7 @@ impl TryFrom for http::Request { IncomingBody::finish(incoming_body); builder .method(req_method) - .uri( - Uri::from_parts(uri_parts) - .map_err(Error::from)?, - ) + .uri(Uri::from_parts(uri_parts).map_err(Error::from)?) .body(Bytes::from(body_bytes)) .map_err(Error::from) } @@ -106,9 +104,7 @@ impl TryFrom for http::uri::Scheme { match value { Scheme::Http => Ok(Self::HTTP), Scheme::Https => Ok(Self::HTTPS), - Scheme::Other(oth) => { - Self::try_from(oth.as_bytes()) - } + Scheme::Other(oth) => Self::try_from(oth.as_bytes()), } } } diff --git a/integrations/wasi/src/response.rs b/integrations/wasi/src/response.rs index fe1856646d..9e96ce0b04 100644 --- a/integrations/wasi/src/response.rs +++ b/integrations/wasi/src/response.rs @@ -14,7 +14,7 @@ use crate::bindings::wasi::http::types::Headers; /// This crate uses platform-agnostic [`http::Response`] /// with a custom [`Body`] and convert them under the hood to /// WASI native types. -/// +/// /// It supports both [`Body::Sync`] and [`Body::Async`], /// allowing you to choose between synchronous response /// (i.e. sending the whole response) and asynchronous response @@ -32,7 +32,8 @@ impl Response { } impl From> for Response - where T: Into, +where + T: Into, { fn from(value: http::Response) -> Self { Self(value.map(Into::into)) @@ -46,14 +47,7 @@ pub enum Body { /// The response body will be written asynchronously, /// this execution model is also known as /// "streaming". - Async( - Pin< - Box< - dyn Stream> - + Send + 'static, - >, - >, - ), + Async(Pin> + Send + 'static>>), } impl From for Body { @@ -65,7 +59,6 @@ impl From for Body { } } - /// This struct lets you define headers and override the status of the Response from an Element or a Server Function /// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. #[derive(Debug, Clone, Default)] @@ -104,13 +97,12 @@ impl ResponseOptions { impl ExtendResponse for Response { type ResponseOptions = ResponseOptions; - fn from_stream(stream: impl Stream + Send + 'static) - -> Self { - let stream = stream - .map(|data| { - Result::::Ok(Bytes::from(data)) - }); - + fn from_stream( + stream: impl Stream + Send + 'static, + ) -> Self { + let stream = + stream.map(|data| Result::::Ok(Bytes::from(data))); + Self(http::Response::new(Body::Async(Box::pin(stream)))) } @@ -119,7 +111,9 @@ impl ExtendResponse for Response { if let Some(status_code) = opt.status { *self.0.status_mut() = status_code; } - self.0.headers_mut().extend(std::mem::take(&mut opt.headers)); + self.0 + .headers_mut() + .extend(std::mem::take(&mut opt.headers)); } fn set_default_content_type(&mut self, content_type: &str) { diff --git a/integrations/wasi/src/utils.rs b/integrations/wasi/src/utils.rs index 22a61d8604..06945803a2 100644 --- a/integrations/wasi/src/utils.rs +++ b/integrations/wasi/src/utils.rs @@ -1,4 +1,8 @@ -use http::{header::{self, ACCEPT}, request::Parts, HeaderName, HeaderValue, StatusCode}; +use http::{ + header::{self, ACCEPT}, + request::Parts, + HeaderName, HeaderValue, StatusCode, +}; use leptos::prelude::use_context; use server_fn::redirect::REDIRECT_HEADER; diff --git a/server_fn/src/request/generic.rs b/server_fn/src/request/generic.rs index c8eef2d3f6..98617f473a 100644 --- a/server_fn/src/request/generic.rs +++ b/server_fn/src/request/generic.rs @@ -1,14 +1,14 @@ //! This module uses platform-agnostic abstractions //! allowing users to run server functions on a wide range of //! platforms. -//! +//! //! The crates in use in this crate are: -//! +//! //! * `bytes`: platform-agnostic manipulation of bytes. //! * `http`: low-dependency HTTP abstractions' *front-end*. -//! +//! //! # Users -//! +//! //! * `wasm32-wasip*` integration crate `leptos_wasi` is using this //! crate under the hood. diff --git a/server_fn/src/response/generic.rs b/server_fn/src/response/generic.rs index 5b95dfef72..f9e10b5f4c 100644 --- a/server_fn/src/response/generic.rs +++ b/server_fn/src/response/generic.rs @@ -1,30 +1,30 @@ //! This module uses platform-agnostic abstractions //! allowing users to run server functions on a wide range of //! platforms. -//! +//! //! The crates in use in this crate are: -//! +//! //! * `bytes`: platform-agnostic manipulation of bytes. //! * `http`: low-dependency HTTP abstractions' *front-end*. -//! +//! //! # Users -//! +//! //! * `wasm32-wasip*` integration crate `leptos_wasi` is using this //! crate under the hood. use super::Res; use crate::error::{ - ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER + ServerFnError, ServerFnErrorErr, ServerFnErrorSerde, SERVER_FN_ERROR_HEADER, }; use bytes::Bytes; use futures::{Stream, TryStreamExt}; use http::{header, HeaderValue, Response, StatusCode}; -use throw_error::Error; use std::{ fmt::{Debug, Display}, pin::Pin, str::FromStr, }; +use throw_error::Error; /// The Body of a Response whose *execution model* can be /// customised using the variants. @@ -35,14 +35,7 @@ pub enum Body { /// The response body will be written asynchronously, /// this execution model is also known as /// "streaming". - Async( - Pin< - Box< - dyn Stream> - + Send + 'static, - >, - >, - ), + Async(Pin> + Send + 'static>>), } impl From for Body { @@ -89,11 +82,9 @@ where builder .status(200) .header(http::header::CONTENT_TYPE, content_type) - .body( - Body::Async( - Box::pin(data.map_err(ServerFnErrorErr::from).map_err(Error::from)) - ) - ) + .body(Body::Async(Box::pin( + data.map_err(ServerFnErrorErr::from).map_err(Error::from), + ))) .map_err(|e| ServerFnError::Response(e.to_string())) } diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index 1e80222a8e..ada7c4a019 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -59,15 +59,14 @@ pub fn server_macro_impl( .inputs .iter_mut() .map(|f| { - let typed_arg = match f { - FnArg::Receiver(_) => { - return Err(syn::Error::new( + let typed_arg = + match f { + FnArg::Receiver(_) => return Err(syn::Error::new( f.span(), "cannot use receiver types in server function macro", - )) - } - FnArg::Typed(t) => t, - }; + )), + FnArg::Typed(t) => t, + }; // strip `mut`, which is allowed in fn args but not in struct fields if let Pat::Ident(ident) = &mut *typed_arg.pat { From 31e114aa041c599cd6757b685a11b6cdea6c1d1e Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Fri, 11 Oct 2024 22:36:10 +0200 Subject: [PATCH 11/23] chore: remove custom clippy Remove clippy crate rules since it seems to make tests fails in tests. Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/src/lib.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs index 731e271118..5ac679f85b 100644 --- a/integrations/wasi/src/lib.rs +++ b/integrations/wasi/src/lib.rs @@ -53,11 +53,6 @@ //! operations to wrap the resource's `handle() -> u64` in //! another type. -#![warn(clippy::all)] -#![warn(clippy::nursery)] -#![allow(clippy::restriction)] -#![deny(clippy::allow_attributes)] - #[allow(warnings)] pub mod bindings { wit_bindgen::generate!({ From 4ecc65af14d7ca6f48d4011bff106eab4daf664d Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Fri, 11 Oct 2024 23:12:23 +0200 Subject: [PATCH 12/23] chore: use `wasi` crate Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/Cargo.toml | 1 + integrations/wasi/src/handler.rs | 9 ++-- integrations/wasi/src/lib.rs | 35 +++------------ integrations/wasi/src/request.rs | 73 ++++++++++++++----------------- integrations/wasi/src/response.rs | 2 +- 5 files changed, 46 insertions(+), 74 deletions(-) diff --git a/integrations/wasi/Cargo.toml b/integrations/wasi/Cargo.toml index 0fafd9a249..6a266ffbbf 100644 --- a/integrations/wasi/Cargo.toml +++ b/integrations/wasi/Cargo.toml @@ -13,6 +13,7 @@ any_spawner = { workspace = true, features = ["futures-executor"] } throw_error = { workspace = true } hydration_context = { workspace = true } futures = "0.3.30" +wasi = "0.13.1+wasi-0.2.0" leptos = { workspace = true, features = ["nonce", "ssr"] } leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } diff --git a/integrations/wasi/src/handler.rs b/integrations/wasi/src/handler.rs index 7b34d6f9c9..4cbfd5cf48 100644 --- a/integrations/wasi/src/handler.rs +++ b/integrations/wasi/src/handler.rs @@ -32,10 +32,11 @@ use routefinder::Router; use server_fn::middleware::Service; use throw_error::Error; +use wasi::http::types::{ + IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, +}; + use crate::{ - bindings::wasi::http::types::{ - IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, - }, response::{Body, Response, ResponseOptions}, utils::redirect, CHUNK_BYTE_SIZE, @@ -120,7 +121,7 @@ impl Handler { res_out: ResponseOutparam, ) -> Result { Ok(Self { - req: req.try_into()?, + req: crate::request::Request(req).try_into()?, res_out, server_fn: None, preset_res: None, diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs index 5ac679f85b..04a3eb16bf 100644 --- a/integrations/wasi/src/lib.rs +++ b/integrations/wasi/src/lib.rs @@ -13,7 +13,7 @@ //! that you can leverage to use this crate. //! //! ``` -//! use leptos_wasi::{bindings::exports::wasi::http::incoming_handler::Guest, prelude::{IncomingRequest, ResponseOutparam}}; +//! use wasi::exports::http::incoming_handler::*; //! //! struct LeptosServer; //! @@ -36,32 +36,7 @@ //! //! # WASI Bindings //! -//! You are free to use any WIT imports and export any WIT exports but at the moment, -//! when interacting with this crate, you must use the types that you can find in -//! this crate [`bindings`]. -//! -//! You then need to export your implementation using: -//! -//! ``` -//! export!(LeptosServer with_types_in leptos_wasi::bindings); -//! ``` -//! -//! If you want to use your own bindings for `wasi:http`, -//! then you need to implement `From` traits -//! to convert your own bindings into the one in [`bindings`]. -//! Please, note that it will likely implies doing `unsafe` -//! operations to wrap the resource's `handle() -> u64` in -//! another type. - -#[allow(warnings)] -pub mod bindings { - wit_bindgen::generate!({ - path: "wit", - pub_export_macro: true, - world: "http", - generate_all, - }); -} +//! We are using the bindings provided by the `wasi` crate. pub mod handler; pub mod request; @@ -70,12 +45,12 @@ pub mod utils; #[allow(clippy::pub_use)] pub mod prelude { - pub use crate::bindings::exports::wasi::http::incoming_handler::{ - IncomingRequest, ResponseOutparam, - }; pub use crate::handler::Handler; pub use crate::response::Body; pub use crate::utils::redirect; + pub use wasi::exports::wasi::http::incoming_handler::{ + IncomingRequest, ResponseOutparam, + }; } /// When working with streams, this crate will try to chunk bytes with diff --git a/integrations/wasi/src/request.rs b/integrations/wasi/src/request.rs index 06ae28bcdd..30346fcbbb 100644 --- a/integrations/wasi/src/request.rs +++ b/integrations/wasi/src/request.rs @@ -1,24 +1,23 @@ use bytes::Bytes; -use http::{ - uri::{InvalidUri, Parts}, - Uri, -}; +use http::{uri::Parts, Uri}; use throw_error::Error; -use crate::{ - bindings::wasi::{ - http::types::{IncomingBody, IncomingRequest, Method, Scheme}, - io::streams::StreamError, - }, - CHUNK_BYTE_SIZE, +use wasi::{ + http::types::{IncomingBody, IncomingRequest, Method, Scheme}, + io::streams::StreamError, }; -impl TryFrom for http::Request { +use crate::CHUNK_BYTE_SIZE; + +pub struct Request(pub IncomingRequest); + +impl TryFrom for http::Request { type Error = Error; - fn try_from(req: IncomingRequest) -> Result { + fn try_from(req: Request) -> Result { let mut builder = http::Request::builder(); - let req_method = req.method(); + let req = req.0; + let req_method = method_wasi_to_http(req.method())?; let headers = req.headers(); for (header_name, header_value) in headers.entries() { @@ -52,8 +51,7 @@ impl TryFrom for http::Request { let mut uri_parts = Parts::default(); - uri_parts.scheme = - req.scheme().map(http::uri::Scheme::try_from).transpose()?; + uri_parts.scheme = req.scheme().map(scheme_wasi_to_http).transpose()?; uri_parts.authority = req .authority() .map(|aut| { @@ -79,32 +77,29 @@ impl TryFrom for http::Request { } } -impl TryFrom for http::Method { - type Error = http::method::InvalidMethod; - - fn try_from(value: Method) -> Result { - match value { - Method::Connect => Ok(Self::CONNECT), - Method::Delete => Ok(Self::DELETE), - Method::Get => Ok(Self::GET), - Method::Head => Ok(Self::HEAD), - Method::Options => Ok(Self::OPTIONS), - Method::Patch => Ok(Self::PATCH), - Method::Post => Ok(Self::POST), - Method::Put => Ok(Self::PUT), - Method::Trace => Ok(Self::TRACE), - Method::Other(mtd) => Self::from_bytes(mtd.as_bytes()), - } +pub fn method_wasi_to_http( + value: Method, +) -> Result { + match value { + Method::Connect => Ok(http::Method::CONNECT), + Method::Delete => Ok(http::Method::DELETE), + Method::Get => Ok(http::Method::GET), + Method::Head => Ok(http::Method::HEAD), + Method::Options => Ok(http::Method::OPTIONS), + Method::Patch => Ok(http::Method::PATCH), + Method::Post => Ok(http::Method::POST), + Method::Put => Ok(http::Method::PUT), + Method::Trace => Ok(http::Method::TRACE), + Method::Other(mtd) => http::Method::from_bytes(mtd.as_bytes()), } } -impl TryFrom for http::uri::Scheme { - type Error = InvalidUri; - fn try_from(value: Scheme) -> Result { - match value { - Scheme::Http => Ok(Self::HTTP), - Scheme::Https => Ok(Self::HTTPS), - Scheme::Other(oth) => Self::try_from(oth.as_bytes()), - } +pub fn scheme_wasi_to_http( + value: Scheme, +) -> Result { + match value { + Scheme::Http => Ok(http::uri::Scheme::HTTP), + Scheme::Https => Ok(http::uri::Scheme::HTTPS), + Scheme::Other(oth) => http::uri::Scheme::try_from(oth.as_bytes()), } } diff --git a/integrations/wasi/src/response.rs b/integrations/wasi/src/response.rs index 9e96ce0b04..b222372712 100644 --- a/integrations/wasi/src/response.rs +++ b/integrations/wasi/src/response.rs @@ -9,7 +9,7 @@ use parking_lot::RwLock; use server_fn::response::generic::Body as ServerFnBody; use throw_error::Error; -use crate::bindings::wasi::http::types::Headers; +use wasi::http::types::Headers; /// This crate uses platform-agnostic [`http::Response`] /// with a custom [`Body`] and convert them under the hood to From 39a25520f7ec6943972adfbe762fc72ea94bcb69 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sat, 19 Oct 2024 13:42:41 +0200 Subject: [PATCH 13/23] chore: revert changes to any_spawner Signed-off-by: Enzo "raskyld" Nocera --- any_spawner/src/lib.rs | 57 +++++++----------------------------------- 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/any_spawner/src/lib.rs b/any_spawner/src/lib.rs index 1fcaee6a8e..a03196a23b 100644 --- a/any_spawner/src/lib.rs +++ b/any_spawner/src/lib.rs @@ -29,9 +29,7 @@ #![deny(missing_docs)] #![cfg_attr(docsrs, feature(doc_cfg))] -use core::{future::Future, panic::Location, pin::Pin}; -use futures::channel::oneshot; -use std::sync::OnceLock; +use std::{future::Future, pin::Pin, sync::OnceLock}; use thiserror::Error; /// A future that has been pinned. @@ -39,18 +37,12 @@ pub type PinnedFuture = Pin + Send>>; /// A future that has been pinned. pub type PinnedLocalFuture = Pin>>; -/// Handle to spawn a new [`PinnedFuture`] on the initiated [`Executor`]. static SPAWN: OnceLock)> = OnceLock::new(); - -/// Handle to spawn a new [`PinnedLocalFuture`] on the initiated [`Executor`]. -/// -/// It is useful when you have a Future that is not [`Send`]. static SPAWN_LOCAL: OnceLock)> = OnceLock::new(); static POLL_LOCAL: OnceLock = OnceLock::new(); /// Errors that can occur when using the executor. #[derive(Error, Debug)] -#[non_exhaustive] pub enum ExecutorError { /// The executor has already been set. #[error("Executor has already been set.")] @@ -70,11 +62,7 @@ impl Executor { /// # } /// ``` #[track_caller] - #[inline] - pub fn spawn(fut: T) - where - T: Future + Send + 'static, - { + pub fn spawn(fut: impl Future + Send + 'static) { if let Some(spawner) = SPAWN.get() { spawner(Box::pin(fut)) } else { @@ -82,13 +70,13 @@ impl Executor { tracing::error!( "At {}, tried to spawn a Future with Executor::spawn() before \ the Executor had been set.", - Location::caller() + std::panic::Location::caller() ); #[cfg(all(debug_assertions, not(feature = "tracing")))] panic!( "At {}, tried to spawn a Future with Executor::spawn() before \ the Executor had been set.", - Location::caller() + std::panic::Location::caller() ); } } @@ -103,11 +91,7 @@ impl Executor { /// # } /// ``` #[track_caller] - #[inline] - pub fn spawn_local(fut: T) - where - T: Future + 'static, - { + pub fn spawn_local(fut: impl Future + 'static) { if let Some(spawner) = SPAWN_LOCAL.get() { spawner(Box::pin(fut)) } else { @@ -115,44 +99,21 @@ impl Executor { tracing::error!( "At {}, tried to spawn a Future with Executor::spawn_local() \ before the Executor had been set.", - Location::caller() + std::panic::Location::caller() ); #[cfg(all(debug_assertions, not(feature = "tracing")))] panic!( "At {}, tried to spawn a Future with Executor::spawn_local() \ before the Executor had been set.", - Location::caller() - ); - } - } - - /// Run the [`Executor`]. - #[track_caller] - #[inline] - pub fn run() { - if let Some(run) = RUN.get() { - run(); - } else { - #[cfg(all(debug_assertions, feature = "tracing"))] - tracing::error!( - "At {}, tried to run an executor with Executor::run() \ - before the Executor had been set.", - Location::caller() - ); - #[cfg(all(debug_assertions, not(feature = "tracing")))] - panic!( - "At {}, tried to run an executor with Executor::run() \ - before the Executor had been set.", - Location::caller() + std::panic::Location::caller() ); } } /// Waits until the next "tick" of the current async executor. - #[inline] pub async fn tick() { - let (tx, rx) = oneshot::channel(); - Self::spawn(async move { + let (tx, rx) = futures::channel::oneshot::channel(); + Executor::spawn(async move { _ = tx.send(()); }); _ = rx.await; From b02e3543110c3568f80018602fcce57b89a8ab2f Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sat, 19 Oct 2024 13:50:53 +0200 Subject: [PATCH 14/23] chore: simpler crate features + cleanup Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/Cargo.toml | 4 +- integrations/wasi/wit/deps.lock | 29 - integrations/wasi/wit/deps.toml | 1 - integrations/wasi/wit/deps/cli/command.wit | 10 - .../wasi/wit/deps/cli/environment.wit | 22 - integrations/wasi/wit/deps/cli/exit.wit | 17 - integrations/wasi/wit/deps/cli/imports.wit | 36 - integrations/wasi/wit/deps/cli/run.wit | 6 - integrations/wasi/wit/deps/cli/stdio.wit | 26 - integrations/wasi/wit/deps/cli/terminal.wit | 62 -- .../wasi/wit/deps/clocks/monotonic-clock.wit | 50 -- .../wasi/wit/deps/clocks/timezone.wit | 55 -- .../wasi/wit/deps/clocks/wall-clock.wit | 46 -- integrations/wasi/wit/deps/clocks/world.wit | 11 - .../wasi/wit/deps/filesystem/preopens.wit | 11 - .../wasi/wit/deps/filesystem/types.wit | 678 ------------------ .../wasi/wit/deps/filesystem/world.wit | 9 - integrations/wasi/wit/deps/http/handler.wit | 49 -- integrations/wasi/wit/deps/http/proxy.wit | 50 -- integrations/wasi/wit/deps/http/types.wit | 655 ----------------- integrations/wasi/wit/deps/io/error.wit | 34 - integrations/wasi/wit/deps/io/poll.wit | 47 -- integrations/wasi/wit/deps/io/streams.wit | 286 -------- integrations/wasi/wit/deps/io/world.wit | 10 - .../wasi/wit/deps/random/insecure-seed.wit | 27 - .../wasi/wit/deps/random/insecure.wit | 25 - integrations/wasi/wit/deps/random/random.wit | 29 - integrations/wasi/wit/deps/random/world.wit | 13 - .../wit/deps/sockets/instance-network.wit | 11 - .../wasi/wit/deps/sockets/ip-name-lookup.wit | 56 -- .../wasi/wit/deps/sockets/network.wit | 153 ---- .../wit/deps/sockets/tcp-create-socket.wit | 30 - integrations/wasi/wit/deps/sockets/tcp.wit | 387 ---------- .../wit/deps/sockets/udp-create-socket.wit | 30 - integrations/wasi/wit/deps/sockets/udp.wit | 288 -------- integrations/wasi/wit/deps/sockets/world.wit | 19 - integrations/wasi/wit/world.wit | 5 - leptos_macro/Cargo.toml | 1 - server_fn/Cargo.toml | 1 - server_fn_macro/Cargo.toml | 1 - 40 files changed, 2 insertions(+), 3278 deletions(-) delete mode 100644 integrations/wasi/wit/deps.lock delete mode 100644 integrations/wasi/wit/deps.toml delete mode 100644 integrations/wasi/wit/deps/cli/command.wit delete mode 100644 integrations/wasi/wit/deps/cli/environment.wit delete mode 100644 integrations/wasi/wit/deps/cli/exit.wit delete mode 100644 integrations/wasi/wit/deps/cli/imports.wit delete mode 100644 integrations/wasi/wit/deps/cli/run.wit delete mode 100644 integrations/wasi/wit/deps/cli/stdio.wit delete mode 100644 integrations/wasi/wit/deps/cli/terminal.wit delete mode 100644 integrations/wasi/wit/deps/clocks/monotonic-clock.wit delete mode 100644 integrations/wasi/wit/deps/clocks/timezone.wit delete mode 100644 integrations/wasi/wit/deps/clocks/wall-clock.wit delete mode 100644 integrations/wasi/wit/deps/clocks/world.wit delete mode 100644 integrations/wasi/wit/deps/filesystem/preopens.wit delete mode 100644 integrations/wasi/wit/deps/filesystem/types.wit delete mode 100644 integrations/wasi/wit/deps/filesystem/world.wit delete mode 100644 integrations/wasi/wit/deps/http/handler.wit delete mode 100644 integrations/wasi/wit/deps/http/proxy.wit delete mode 100644 integrations/wasi/wit/deps/http/types.wit delete mode 100644 integrations/wasi/wit/deps/io/error.wit delete mode 100644 integrations/wasi/wit/deps/io/poll.wit delete mode 100644 integrations/wasi/wit/deps/io/streams.wit delete mode 100644 integrations/wasi/wit/deps/io/world.wit delete mode 100644 integrations/wasi/wit/deps/random/insecure-seed.wit delete mode 100644 integrations/wasi/wit/deps/random/insecure.wit delete mode 100644 integrations/wasi/wit/deps/random/random.wit delete mode 100644 integrations/wasi/wit/deps/random/world.wit delete mode 100644 integrations/wasi/wit/deps/sockets/instance-network.wit delete mode 100644 integrations/wasi/wit/deps/sockets/ip-name-lookup.wit delete mode 100644 integrations/wasi/wit/deps/sockets/network.wit delete mode 100644 integrations/wasi/wit/deps/sockets/tcp-create-socket.wit delete mode 100644 integrations/wasi/wit/deps/sockets/tcp.wit delete mode 100644 integrations/wasi/wit/deps/sockets/udp-create-socket.wit delete mode 100644 integrations/wasi/wit/deps/sockets/udp.wit delete mode 100644 integrations/wasi/wit/deps/sockets/world.wit delete mode 100644 integrations/wasi/wit/world.wit diff --git a/integrations/wasi/Cargo.toml b/integrations/wasi/Cargo.toml index 6a266ffbbf..c582c48c10 100644 --- a/integrations/wasi/Cargo.toml +++ b/integrations/wasi/Cargo.toml @@ -17,9 +17,9 @@ wasi = "0.13.1+wasi-0.2.0" leptos = { workspace = true, features = ["nonce", "ssr"] } leptos_meta = { workspace = true, features = ["ssr"] } leptos_router = { workspace = true, features = ["ssr"] } -leptos_macro = { workspace = true, features = ["wasi"] } +leptos_macro = { workspace = true, features = ["generic"] } leptos_integration_utils = { workspace = true } -server_fn = { workspace = true, features = ["wasi"] } +server_fn = { workspace = true, features = ["generic"] } wit-bindgen = "0.32.0" http = "1.1.0" parking_lot = "0.12.3" diff --git a/integrations/wasi/wit/deps.lock b/integrations/wasi/wit/deps.lock deleted file mode 100644 index 463fc62133..0000000000 --- a/integrations/wasi/wit/deps.lock +++ /dev/null @@ -1,29 +0,0 @@ -[cli] -sha256 = "1de50b8e6940e73110cda10b7f90ca87a8fea886f0fa36c748f96dc70671ee38" -sha512 = "bbb6cd3e7b4d3237b6af9bfbb2633ccd2c4ea2a4f37b8c033255c7e0c1cb037be7f22ec1f8ca792cc8ec1942199582943979e646b4b272b85dcff7654eac51d0" - -[clocks] -sha256 = "ea9d69ee803bc176e23e5268f5e24a2ac485dd1f62a0ab4c748e9d3f901f576f" -sha512 = "5efc22927c46cd56c41e5549ec775561c7fac2ea0d365abc0b55396d9475a7c9f984077a81f84a44a726f1c008fd2fadbffffa4fa53ecd5fbfd05afd379ab428" - -[filesystem] -sha256 = "cfe8c420e8b857de612ae2a3336680dae16b95c93c8ba3a6ff05b21210966740" -sha512 = "3c00c5544a58658e3e8025677091685286027fd49f37abf198c30b4e83b9e68f19723975aaa98794fba9f425ae9ef4f3dc0f5b9cf59203b5ecfaadf62b296f9a" - -[http] -url = "https://github.com/WebAssembly/wasi-http/archive/v0.2.1.tar.gz" -sha256 = "ff21e05571af02488371bebc9cb674e30e0613eda22e3c1d8e1aba36cdff5ee8" -sha512 = "1825b56f1718e822acf1b49929ead8f6493752b1d3524ce0974c3acdf656af2fc3fe5c8456b71ffab44583bc3ae7061d5a285d8a7203fcae949d44b3d81e2f2b" -deps = ["cli", "clocks", "filesystem", "io", "random", "sockets"] - -[io] -sha256 = "2a74bd811adc46b5a0f19827ddbde89870e52b17615f4d0873f06fd977250caf" -sha512 = "94624f00c66e66203592cee820f80b1ba91ecdb71f682c154f25eaf71f8d8954197dcb64503bc21e72ed5e812af7eae876df47b7eb727b02db3a74a7ce0aefca" - -[random] -sha256 = "9e2d5056186f81b2e7f96bc97d2babd0341840f6abb4f170449b70992f1b598f" -sha512 = "67bf41d8d5d4b7af084124ee85196585225785969059f59e2f9ddb77ac1a8095cfe811ae29d076aac817418fa01064d7b9fbc0233930bace680758eeb21e36f8" - -[sockets] -sha256 = "4c361137a7e61e8b9a73da2a0899dd9ad1a0c2dfee7d310cf168704c57b7a07c" -sha512 = "348b4ef381f57aec23d48537df8b69ab8963587dcb056e94c4cd5657e217677a4ee2a545868a5c829d2334cc6b8b0a61d3e72797999f44d78553fbd3a73c5b8d" diff --git a/integrations/wasi/wit/deps.toml b/integrations/wasi/wit/deps.toml deleted file mode 100644 index 77a7d114c2..0000000000 --- a/integrations/wasi/wit/deps.toml +++ /dev/null @@ -1 +0,0 @@ -http = "https://github.com/WebAssembly/wasi-http/archive/v0.2.1.tar.gz" diff --git a/integrations/wasi/wit/deps/cli/command.wit b/integrations/wasi/wit/deps/cli/command.wit deleted file mode 100644 index dc064a3cd9..0000000000 --- a/integrations/wasi/wit/deps/cli/command.wit +++ /dev/null @@ -1,10 +0,0 @@ -package wasi:cli@0.2.1; - -@since(version = 0.2.0) -world command { - @since(version = 0.2.0) - include imports; - - @since(version = 0.2.0) - export run; -} diff --git a/integrations/wasi/wit/deps/cli/environment.wit b/integrations/wasi/wit/deps/cli/environment.wit deleted file mode 100644 index 2f449bd7c1..0000000000 --- a/integrations/wasi/wit/deps/cli/environment.wit +++ /dev/null @@ -1,22 +0,0 @@ -@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/integrations/wasi/wit/deps/cli/exit.wit b/integrations/wasi/wit/deps/cli/exit.wit deleted file mode 100644 index 427935c8d0..0000000000 --- a/integrations/wasi/wit/deps/cli/exit.wit +++ /dev/null @@ -1,17 +0,0 @@ -@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/integrations/wasi/wit/deps/cli/imports.wit b/integrations/wasi/wit/deps/cli/imports.wit deleted file mode 100644 index b8339d3b22..0000000000 --- a/integrations/wasi/wit/deps/cli/imports.wit +++ /dev/null @@ -1,36 +0,0 @@ -package wasi:cli@0.2.1; - -@since(version = 0.2.0) -world imports { - @since(version = 0.2.0) - include wasi:clocks/imports@0.2.1; - @since(version = 0.2.0) - include wasi:filesystem/imports@0.2.1; - @since(version = 0.2.0) - include wasi:sockets/imports@0.2.1; - @since(version = 0.2.0) - include wasi:random/imports@0.2.1; - @since(version = 0.2.0) - include wasi:io/imports@0.2.1; - - @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/integrations/wasi/wit/deps/cli/run.wit b/integrations/wasi/wit/deps/cli/run.wit deleted file mode 100644 index 655346efb6..0000000000 --- a/integrations/wasi/wit/deps/cli/run.wit +++ /dev/null @@ -1,6 +0,0 @@ -@since(version = 0.2.0) -interface run { - /// Run the program. - @since(version = 0.2.0) - run: func() -> result; -} diff --git a/integrations/wasi/wit/deps/cli/stdio.wit b/integrations/wasi/wit/deps/cli/stdio.wit deleted file mode 100644 index d1d26eb615..0000000000 --- a/integrations/wasi/wit/deps/cli/stdio.wit +++ /dev/null @@ -1,26 +0,0 @@ -@since(version = 0.2.0) -interface stdin { - @since(version = 0.2.0) - use wasi:io/streams@0.2.1.{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.1.{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.1.{output-stream}; - - @since(version = 0.2.0) - get-stderr: func() -> output-stream; -} diff --git a/integrations/wasi/wit/deps/cli/terminal.wit b/integrations/wasi/wit/deps/cli/terminal.wit deleted file mode 100644 index d305498c64..0000000000 --- a/integrations/wasi/wit/deps/cli/terminal.wit +++ /dev/null @@ -1,62 +0,0 @@ -/// 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/integrations/wasi/wit/deps/clocks/monotonic-clock.wit b/integrations/wasi/wit/deps/clocks/monotonic-clock.wit deleted file mode 100644 index 3c24840c98..0000000000 --- a/integrations/wasi/wit/deps/clocks/monotonic-clock.wit +++ /dev/null @@ -1,50 +0,0 @@ -package wasi:clocks@0.2.1; -/// 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.1.{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/integrations/wasi/wit/deps/clocks/timezone.wit b/integrations/wasi/wit/deps/clocks/timezone.wit deleted file mode 100644 index 212da66821..0000000000 --- a/integrations/wasi/wit/deps/clocks/timezone.wit +++ /dev/null @@ -1,55 +0,0 @@ -package wasi:clocks@0.2.1; - -@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/integrations/wasi/wit/deps/clocks/wall-clock.wit b/integrations/wasi/wit/deps/clocks/wall-clock.wit deleted file mode 100644 index 6be069a327..0000000000 --- a/integrations/wasi/wit/deps/clocks/wall-clock.wit +++ /dev/null @@ -1,46 +0,0 @@ -package wasi:clocks@0.2.1; -/// 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/integrations/wasi/wit/deps/clocks/world.wit b/integrations/wasi/wit/deps/clocks/world.wit deleted file mode 100644 index 9251ac6456..0000000000 --- a/integrations/wasi/wit/deps/clocks/world.wit +++ /dev/null @@ -1,11 +0,0 @@ -package wasi:clocks@0.2.1; - -@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/integrations/wasi/wit/deps/filesystem/preopens.wit b/integrations/wasi/wit/deps/filesystem/preopens.wit deleted file mode 100644 index ca2f726af9..0000000000 --- a/integrations/wasi/wit/deps/filesystem/preopens.wit +++ /dev/null @@ -1,11 +0,0 @@ -package wasi:filesystem@0.2.1; - -@since(version = 0.2.0) -interface preopens { - @since(version = 0.2.0) - use types.{descriptor}; - - /// Return the set of preopened directories, and their path. - @since(version = 0.2.0) - get-directories: func() -> list>; -} diff --git a/integrations/wasi/wit/deps/filesystem/types.wit b/integrations/wasi/wit/deps/filesystem/types.wit deleted file mode 100644 index db3d96867b..0000000000 --- a/integrations/wasi/wit/deps/filesystem/types.wit +++ /dev/null @@ -1,678 +0,0 @@ -package wasi:filesystem@0.2.1; -/// 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.1.{input-stream, output-stream, error}; - @since(version = 0.2.0) - use wasi:clocks/wall-clock@0.2.1.{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 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. - /// - /// 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. - /// - /// The returned descriptor is not guaranteed to be the lowest-numbered - /// descriptor not currently open/ it is randomized to prevent applications - /// from depending on making assumptions about indexes, since this is - /// error-prone in multi-threaded contexts. The returned descriptor is - /// guaranteed to be less than 2**31. - /// - /// 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 encourated 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/integrations/wasi/wit/deps/filesystem/world.wit b/integrations/wasi/wit/deps/filesystem/world.wit deleted file mode 100644 index af0146cbc9..0000000000 --- a/integrations/wasi/wit/deps/filesystem/world.wit +++ /dev/null @@ -1,9 +0,0 @@ -package wasi:filesystem@0.2.1; - -@since(version = 0.2.0) -world imports { - @since(version = 0.2.0) - import types; - @since(version = 0.2.0) - import preopens; -} diff --git a/integrations/wasi/wit/deps/http/handler.wit b/integrations/wasi/wit/deps/http/handler.wit deleted file mode 100644 index 6a6c62966f..0000000000 --- a/integrations/wasi/wit/deps/http/handler.wit +++ /dev/null @@ -1,49 +0,0 @@ -/// 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/integrations/wasi/wit/deps/http/proxy.wit b/integrations/wasi/wit/deps/http/proxy.wit deleted file mode 100644 index 415d2ee1cb..0000000000 --- a/integrations/wasi/wit/deps/http/proxy.wit +++ /dev/null @@ -1,50 +0,0 @@ -package wasi:http@0.2.1; - -/// 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.1; - @since(version = 0.2.0) - import wasi:clocks/wall-clock@0.2.1; - @since(version = 0.2.0) - import wasi:random/random@0.2.1; - - /// 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.1; - @since(version = 0.2.0) - import wasi:cli/stderr@0.2.1; - - /// 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.1; - - /// 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/integrations/wasi/wit/deps/http/types.wit b/integrations/wasi/wit/deps/http/types.wit deleted file mode 100644 index 3c45cd08b7..0000000000 --- a/integrations/wasi/wit/deps/http/types.wit +++ /dev/null @@ -1,655 +0,0 @@ -/// 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.1.{duration}; - @since(version = 0.2.0) - use wasi:io/streams@0.2.1.{input-stream, output-stream}; - @since(version = 0.2.0) - use wasi:io/error@0.2.1.{error as io-error}; - @since(version = 0.2.0) - use wasi:io/poll@0.2.1.{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: - /// https://www.iana.org/assignments/http-proxy-status/http-proxy-status.xhtml#table-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-key` 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-key` 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 keys are always strings. - @since(version = 0.2.0) - 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 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 key-value pair in the Fields. Keys - /// which have multiple values are represented by multiple entries in this - /// list with the same key. - /// - /// The tuple is a pair of the field key, represented as a string, and - /// Value, represented as a list of bytes. - /// - /// An error result will be returned if any `field-key` 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 key. If the key is not present - /// in this `fields` or is syntactically invalid, an empty list is returned. - /// However, if the key 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-key) -> list; - - /// Returns `true` when the key is present in this `fields`. If the key is - /// syntactically invalid, `false` is returned. - @since(version = 0.2.0) - has: func(name: field-key) -> bool; - - /// Set all of the values for a key. Clears any existing values for that - /// key, if they have been set. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.invalid-syntax` if the `field-key` or any of - /// the `field-value`s are syntactically invalid. - @since(version = 0.2.0) - set: func(name: field-key, value: list) -> result<_, header-error>; - - /// Delete all values for a key. Does nothing if no values for the key - /// exist. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.invalid-syntax` if the `field-key` is - /// syntactically invalid. - @since(version = 0.2.0) - delete: func(name: field-key) -> result<_, header-error>; - - /// Append a value for a key. Does not change or delete any existing - /// values for that key. - /// - /// Fails with `header-error.immutable` if the `fields` are immutable. - /// - /// Fails with `header-error.invalid-syntax` if the `field-key` or - /// `field-value` are syntactically invalid. - @since(version = 0.2.0) - append: func(name: field-key, value: field-value) -> result<_, header-error>; - - /// Retrieve the full set of keys and values in the Fields. Like the - /// constructor, the list represents each key-value pair. - /// - /// The outer list represents each key-value pair in the Fields. Keys - /// which have multiple values are represented by multiple entries in this - /// list with the same key. - @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 { - - /// 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/integrations/wasi/wit/deps/io/error.wit b/integrations/wasi/wit/deps/io/error.wit deleted file mode 100644 index 4ea29c4696..0000000000 --- a/integrations/wasi/wit/deps/io/error.wit +++ /dev/null @@ -1,34 +0,0 @@ -package wasi:io@0.2.1; - -@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/integrations/wasi/wit/deps/io/poll.wit b/integrations/wasi/wit/deps/io/poll.wit deleted file mode 100644 index b25ac729ff..0000000000 --- a/integrations/wasi/wit/deps/io/poll.wit +++ /dev/null @@ -1,47 +0,0 @@ -package wasi:io@0.2.1; - -/// 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/integrations/wasi/wit/deps/io/streams.wit b/integrations/wasi/wit/deps/io/streams.wit deleted file mode 100644 index b697e24d61..0000000000 --- a/integrations/wasi/wit/deps/io/streams.wit +++ /dev/null @@ -1,286 +0,0 @@ -package wasi:io@0.2.1; - -/// 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. - 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. - 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/integrations/wasi/wit/deps/io/world.wit b/integrations/wasi/wit/deps/io/world.wit deleted file mode 100644 index 6405a4e482..0000000000 --- a/integrations/wasi/wit/deps/io/world.wit +++ /dev/null @@ -1,10 +0,0 @@ -package wasi:io@0.2.1; - -@since(version = 0.2.0) -world imports { - @since(version = 0.2.0) - import streams; - - @since(version = 0.2.0) - import poll; -} diff --git a/integrations/wasi/wit/deps/random/insecure-seed.wit b/integrations/wasi/wit/deps/random/insecure-seed.wit deleted file mode 100644 index 7e708dc52b..0000000000 --- a/integrations/wasi/wit/deps/random/insecure-seed.wit +++ /dev/null @@ -1,27 +0,0 @@ -package wasi:random@0.2.1; -/// 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/integrations/wasi/wit/deps/random/insecure.wit b/integrations/wasi/wit/deps/random/insecure.wit deleted file mode 100644 index 3cdb53dfbb..0000000000 --- a/integrations/wasi/wit/deps/random/insecure.wit +++ /dev/null @@ -1,25 +0,0 @@ -package wasi:random@0.2.1; -/// 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/integrations/wasi/wit/deps/random/random.wit b/integrations/wasi/wit/deps/random/random.wit deleted file mode 100644 index 2b5035d1ce..0000000000 --- a/integrations/wasi/wit/deps/random/random.wit +++ /dev/null @@ -1,29 +0,0 @@ -package wasi:random@0.2.1; -/// 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/integrations/wasi/wit/deps/random/world.wit b/integrations/wasi/wit/deps/random/world.wit deleted file mode 100644 index c615e96dc7..0000000000 --- a/integrations/wasi/wit/deps/random/world.wit +++ /dev/null @@ -1,13 +0,0 @@ -package wasi:random@0.2.1; - -@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/integrations/wasi/wit/deps/sockets/instance-network.wit b/integrations/wasi/wit/deps/sockets/instance-network.wit deleted file mode 100644 index 5f6e6c1cc9..0000000000 --- a/integrations/wasi/wit/deps/sockets/instance-network.wit +++ /dev/null @@ -1,11 +0,0 @@ - -/// 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/integrations/wasi/wit/deps/sockets/ip-name-lookup.wit b/integrations/wasi/wit/deps/sockets/ip-name-lookup.wit deleted file mode 100644 index dc56f30007..0000000000 --- a/integrations/wasi/wit/deps/sockets/ip-name-lookup.wit +++ /dev/null @@ -1,56 +0,0 @@ -@since(version = 0.2.0) -interface ip-name-lookup { - @since(version = 0.2.0) - use wasi:io/poll@0.2.1.{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 Preview2 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/integrations/wasi/wit/deps/sockets/network.wit b/integrations/wasi/wit/deps/sockets/network.wit deleted file mode 100644 index 8c13b348e5..0000000000 --- a/integrations/wasi/wit/deps/sockets/network.wit +++ /dev/null @@ -1,153 +0,0 @@ -@since(version = 0.2.0) -interface network { - /// 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, - } - - @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/integrations/wasi/wit/deps/sockets/tcp-create-socket.wit b/integrations/wasi/wit/deps/sockets/tcp-create-socket.wit deleted file mode 100644 index eedbd30768..0000000000 --- a/integrations/wasi/wit/deps/sockets/tcp-create-socket.wit +++ /dev/null @@ -1,30 +0,0 @@ -@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/integrations/wasi/wit/deps/sockets/tcp.wit b/integrations/wasi/wit/deps/sockets/tcp.wit deleted file mode 100644 index bae5a29ece..0000000000 --- a/integrations/wasi/wit/deps/sockets/tcp.wit +++ /dev/null @@ -1,387 +0,0 @@ -@since(version = 0.2.0) -interface tcp { - @since(version = 0.2.0) - use wasi:io/streams@0.2.1.{input-stream, output-stream}; - @since(version = 0.2.0) - use wasi:io/poll@0.2.1.{pollable}; - @since(version = 0.2.0) - use wasi:clocks/monotonic-clock@0.2.1.{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 Preview2 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/integrations/wasi/wit/deps/sockets/udp-create-socket.wit b/integrations/wasi/wit/deps/sockets/udp-create-socket.wit deleted file mode 100644 index e8eeacbfef..0000000000 --- a/integrations/wasi/wit/deps/sockets/udp-create-socket.wit +++ /dev/null @@ -1,30 +0,0 @@ -@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/integrations/wasi/wit/deps/sockets/udp.wit b/integrations/wasi/wit/deps/sockets/udp.wit deleted file mode 100644 index b289e4943f..0000000000 --- a/integrations/wasi/wit/deps/sockets/udp.wit +++ /dev/null @@ -1,288 +0,0 @@ -@since(version = 0.2.0) -interface udp { - @since(version = 0.2.0) - use wasi:io/poll@0.2.1.{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 Preview2 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 Preview2 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 Preview2 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/integrations/wasi/wit/deps/sockets/world.wit b/integrations/wasi/wit/deps/sockets/world.wit deleted file mode 100644 index a1d42670e6..0000000000 --- a/integrations/wasi/wit/deps/sockets/world.wit +++ /dev/null @@ -1,19 +0,0 @@ -package wasi:sockets@0.2.1; - -@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/integrations/wasi/wit/world.wit b/integrations/wasi/wit/world.wit deleted file mode 100644 index 4889b8f16c..0000000000 --- a/integrations/wasi/wit/world.wit +++ /dev/null @@ -1,5 +0,0 @@ -package leptos:server; - -world http { - include wasi:http/proxy@0.2.1; -} diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 596122072b..556e7ce550 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -49,7 +49,6 @@ trace-component-props = [] actix = ["server_fn_macro/actix"] axum = ["server_fn_macro/axum"] generic = ["server_fn_macro/generic"] -wasi = ["server_fn_macro/wasi"] [package.metadata.cargo-all-features] denylist = ["nightly", "tracing", "trace-component-props"] diff --git a/server_fn/Cargo.toml b/server_fn/Cargo.toml index a96fc6cd60..b15f58498b 100644 --- a/server_fn/Cargo.toml +++ b/server_fn/Cargo.toml @@ -111,7 +111,6 @@ default-tls = ["reqwest?/default-tls"] rustls = ["reqwest?/rustls-tls"] reqwest = ["dep:reqwest"] ssr = ["inventory"] -wasi = ["generic"] generic = [] [package.metadata.docs.rs] diff --git a/server_fn_macro/Cargo.toml b/server_fn_macro/Cargo.toml index b12a1823fa..97a8137580 100644 --- a/server_fn_macro/Cargo.toml +++ b/server_fn_macro/Cargo.toml @@ -21,7 +21,6 @@ nightly = [] ssr = [] actix = [] axum = [] -wasi = ["generic"] generic = [] reqwest = [] From ae0ff047aef4af5303821ae4d8d5241354691e40 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sun, 20 Oct 2024 17:28:43 +0200 Subject: [PATCH 15/23] feat(any_spawner): add local custom executor This commit adds a single-thread "local" custom executor, which is useful for environments like `wasm32` targets. Signed-off-by: Enzo "raskyld" Nocera --- any_spawner/src/lib.rs | 46 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/any_spawner/src/lib.rs b/any_spawner/src/lib.rs index a03196a23b..97a61ba714 100644 --- a/any_spawner/src/lib.rs +++ b/any_spawner/src/lib.rs @@ -291,9 +291,9 @@ impl Executor { /// /// Returns `Err(_)` if an executor has already been set. pub fn init_custom_executor( - custom_executor: impl CustomExecutor + 'static, + custom_executor: impl CustomExecutor + Send + Sync + 'static, ) -> Result<(), ExecutorError> { - static EXECUTOR: OnceLock> = OnceLock::new(); + static EXECUTOR: OnceLock> = OnceLock::new(); EXECUTOR .set(Box::new(custom_executor)) .map_err(|_| ExecutorError::AlreadySet)?; @@ -311,13 +311,53 @@ impl Executor { .map_err(|_| ExecutorError::AlreadySet)?; Ok(()) } + + /// Locally sets a custom executor as the executor used to spawn tasks + /// in the current thread. + /// + /// Returns `Err(_)` if an executor has already been set. + pub fn init_local_custom_executor( + custom_executor: impl CustomExecutor + 'static, + ) -> Result<(), ExecutorError> { + thread_local! { + static EXECUTOR: OnceLock> = OnceLock::new(); + } + EXECUTOR.with(|this| { + this + .set(Box::new(custom_executor)) + .map_err(|_| ExecutorError::AlreadySet) + })?; + + SPAWN + .set(|fut| { + EXECUTOR.with(|this| { + this.get().unwrap().spawn(fut) + }); + }) + .map_err(|_| ExecutorError::AlreadySet)?; + SPAWN_LOCAL + .set(|fut| { + EXECUTOR.with(|this| { + this.get().unwrap().spawn_local(fut) + }); + }) + .map_err(|_| ExecutorError::AlreadySet)?; + POLL_LOCAL + .set(|| { + EXECUTOR.with(|this| { + this.get().unwrap().poll_local() + }); + }) + .map_err(|_| ExecutorError::AlreadySet)?; + Ok(()) + } } /// A trait for custom executors. /// Custom executors can be used to integrate with any executor that supports spawning futures. /// /// All methods can be called recursively. -pub trait CustomExecutor: Send + Sync { +pub trait CustomExecutor { /// Spawns a future, usually on a thread pool. fn spawn(&self, fut: PinnedFuture<()>); /// Spawns a local future. May require calling `poll_local` to make progress. From 5db62339554fba8c6e6d392e328758e7dd2bc8b3 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Sun, 20 Oct 2024 17:38:05 +0200 Subject: [PATCH 16/23] feat(leptos_wasi): async runtime This commit adds a single-threaded async runtime for `wasm32-wasip*` targets. Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/src/executor.rs | 258 ++++++++++++++++++++++++++++++ integrations/wasi/src/lib.rs | 2 + 2 files changed, 260 insertions(+) create mode 100644 integrations/wasi/src/executor.rs diff --git a/integrations/wasi/src/executor.rs b/integrations/wasi/src/executor.rs new file mode 100644 index 0000000000..ffed675ebd --- /dev/null +++ b/integrations/wasi/src/executor.rs @@ -0,0 +1,258 @@ +//! This is (Yet Another) Async Runtime for WASI with first-class support +//! for `.await`-ing on [`Pollable`]. It is an ad-hoc implementation +//! tailored for Leptos but it could be exported into a standalone crate. +//! +//! It is based on the `futures` crate's [`LocalPool`] and makes use of +//! no `unsafe` code. +//! +//! # Performance Notes +//! +//! I haven't benchmarked this runtime but since it makes no use of unsafe code +//! and Rust `core`'s `Context` was prematurely optimised for multi-threading +//! environment, I had no choice but using synchronisation primitives to make +//! the API happy. +//! +//! IIRC, `wasm32` targets have an implementation of synchronisation primitives +//! that are just stubs, downgrading them to their single-threaded counterpart +//! so the overhead should be minimal. +//! +//! Also, you can customise the behaviour of the [`Executor`] using the +//! [`Mode`] enum to trade-off reactivity for less host context switch +//! with the [`Mode::Stalled`] variant. + +use std::{cell::RefCell, future::Future, mem, rc::Rc, sync::{Arc, OnceLock}, task::{Context, Poll, Wake, Waker}}; + +use any_spawner::CustomExecutor; +use futures::{channel::mpsc::{UnboundedReceiver, UnboundedSender}, executor::{LocalPool, LocalSpawner}, task::{LocalSpawnExt, SpawnExt}, FutureExt, Stream}; +use parking_lot::Mutex; +use wasi::{clocks::monotonic_clock::{subscribe_duration, Duration}, io::poll::{poll, Pollable}}; + +struct TableEntry(Pollable, Waker); + +static POLLABLE_SINK: OnceLock> = OnceLock::new(); + +pub async fn sleep(duration: Duration) { + WaitPoll::new(subscribe_duration(duration)).await +} + +pub struct WaitPoll(WaitPollInner); + +enum WaitPollInner { + Unregistered(Pollable), + Registered(Arc), +} + +impl WaitPoll { + pub fn new(pollable: Pollable) -> Self { + Self(WaitPollInner::Unregistered(pollable)) + } +} + +impl Future for WaitPoll { + type Output = (); + + fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match &mut self.get_mut().0 { + this @ WaitPollInner::Unregistered(_) => { + let waker = Arc::new(WaitPollWaker::new(cx.waker())); + + if let Some(sender) = POLLABLE_SINK.get() { + if let WaitPollInner::Unregistered(pollable) = mem::replace(this, WaitPollInner::Registered(waker.clone())) { + sender + .clone() + .unbounded_send( + TableEntry( + pollable, + waker.into(), + ) + ) + .expect("cannot spawn a new WaitPoll"); + + Poll::Pending + } else { + unreachable!(); + } + } else { + panic!("cannot create a WaitPoll before creating an Executor"); + } + }, + WaitPollInner::Registered(waker) => { + let mut lock = waker.0.lock(); + if lock.done { + Poll::Ready(()) + } else { + // How can it happen?! :O + // Well, if, for some reason, the Task get woken up for + // another reason than the pollable associated with this + // WaitPoll got ready. + // + // We need to make sure we update the waker. + lock.task_waker = cx.waker().clone(); + Poll::Pending + } + } + } + } +} + +struct WaitPollWaker(Mutex); + +struct WaitPollWakerInner { + done: bool, + task_waker: Waker, +} + +impl WaitPollWaker { + fn new(waker: &Waker) -> Self { + Self(Mutex::new(WaitPollWakerInner { + done: false, + task_waker: waker.clone() + })) + } +} + +impl Wake for WaitPollWaker { + fn wake(self: std::sync::Arc) { + self.wake_by_ref(); + } + + fn wake_by_ref(self: &std::sync::Arc) { + let mut lock = self.0.lock(); + lock.task_waker.wake_by_ref(); + lock.done = true; + } +} + +/// Controls how often the [`Executor`] checks for [`Pollable`] readiness. +pub enum Mode { + /// Will check as often as possible for readiness, this have some + /// performance overhead. + Premptive, + + /// Will only check for readiness when no more progress can be made + /// on pooled Futures. + Stalled, +} + +#[derive(Clone)] +pub struct Executor(Rc); + +struct ExecutorInner { + pool: RefCell, + spawner: LocalSpawner, + rx: RefCell>, + mode: Mode, +} + +impl Executor { + pub fn new(mode: Mode) -> Self { + let pool = LocalPool::new(); + let spawner = pool.spawner(); + let (tx, rx) = futures::channel::mpsc::unbounded(); + + POLLABLE_SINK + .set(tx.clone()) + .expect("calling Executor::new two times is not supported"); + + Self(Rc::new(ExecutorInner { + pool: RefCell::new(pool), + spawner, + rx: RefCell::new(rx), + mode, + })) + } + + pub fn run_until(&self, fut: T) -> T::Output + where + T: Future + 'static, + { + let (tx, mut rx) = futures::channel::oneshot::channel::(); + self.spawn_local(Box::pin(fut.then(|val| async move { + if tx.send(val).is_err() { + panic!("failed to send the return value of the future passed to run_until"); + } + }))); + + loop { + match rx.try_recv() { + Err(_) => panic!("internal error: sender of run until has been dropped"), + Ok(Some(val)) => return val, + Ok(None) => { + self.poll_local(); + } + } + } + } +} + +impl CustomExecutor for Executor { + fn spawn(&self, fut: any_spawner::PinnedFuture<()>) { + self.0.spawner.spawn(fut).unwrap(); + } + + fn spawn_local(&self, fut: any_spawner::PinnedLocalFuture<()>) { + self.0.spawner.spawn_local(fut).unwrap(); + } + + fn poll_local(&self) { + let mut pool = match self.0.pool.try_borrow_mut() { + Ok(pool) => pool, + // Nested call to poll_local(), noop. + Err(_) => return, + }; + + match self.0.mode { + Mode::Premptive => { pool.try_run_one(); }, + Mode::Stalled => pool.run_until_stalled(), + }; + + let (lower, upper) = self.0.rx.borrow().size_hint(); + let capacity = upper.unwrap_or(lower); + let mut entries = Vec::with_capacity(capacity); + let mut rx = self.0.rx.borrow_mut(); + + loop { + match rx.try_next() { + Ok(None) => break, + Ok(Some(entry)) => { + entries.push(Some(entry)); + }, + Err(_) => break, + } + } + + if entries.is_empty() { + // This could happen if some Futures use Waker that are not + // registered through [`WaitPoll`] or that we are blocked + // because some Future returned `Poll::Pending` without + // actually making sure their Waker is called at some point. + return; + } + + let pollables = entries + .iter() + .map(|entry| &entry.as_ref().unwrap().0) + .collect::>(); + + let ready = poll(&pollables); + + if let Some(sender) = POLLABLE_SINK.get() { + let sender = sender.clone(); + + // Wakes futures subscribed to ready pollable. + for index in ready { + let wake = entries[index as usize].take().unwrap().1; + wake.wake(); + } + + // Requeue not ready pollable. + for entry in entries.into_iter().flatten() { + sender + .unbounded_send(entry) + .expect("the sender channel is closed"); + } + } else { + unreachable!(); + } + } +} diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs index 04a3eb16bf..4fa9ba2f0a 100644 --- a/integrations/wasi/src/lib.rs +++ b/integrations/wasi/src/lib.rs @@ -42,6 +42,7 @@ pub mod handler; pub mod request; pub mod response; pub mod utils; +pub mod executor; #[allow(clippy::pub_use)] pub mod prelude { @@ -51,6 +52,7 @@ pub mod prelude { pub use wasi::exports::wasi::http::incoming_handler::{ IncomingRequest, ResponseOutparam, }; + pub use crate::executor::Executor as WasiExecutor; } /// When working with streams, this crate will try to chunk bytes with From c2254af306faf423ae831222670ca9a7bbdadd74 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Wed, 23 Oct 2024 22:17:09 +0200 Subject: [PATCH 17/23] feat(leptos_wasi): error handling This commit adds error types for the users to implement better error handling. Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/Cargo.toml | 2 +- integrations/wasi/src/executor.rs | 76 ++++++++++++++++++++----------- integrations/wasi/src/handler.rs | 41 +++++++++++------ integrations/wasi/src/lib.rs | 4 +- integrations/wasi/src/request.rs | 35 +++++++++----- integrations/wasi/src/response.rs | 28 +++++++++--- integrations/wasi/src/utils.rs | 10 ++-- 7 files changed, 128 insertions(+), 68 deletions(-) diff --git a/integrations/wasi/Cargo.toml b/integrations/wasi/Cargo.toml index c582c48c10..2e97ad2f6f 100644 --- a/integrations/wasi/Cargo.toml +++ b/integrations/wasi/Cargo.toml @@ -20,12 +20,12 @@ leptos_router = { workspace = true, features = ["ssr"] } leptos_macro = { workspace = true, features = ["generic"] } leptos_integration_utils = { workspace = true } server_fn = { workspace = true, features = ["generic"] } -wit-bindgen = "0.32.0" http = "1.1.0" parking_lot = "0.12.3" bytes = "1.7.2" routefinder = "0.5.4" mime_guess = "2.0" +thiserror = "1.0.65" [features] islands-router = [] diff --git a/integrations/wasi/src/executor.rs b/integrations/wasi/src/executor.rs index ffed675ebd..8c604f83ab 100644 --- a/integrations/wasi/src/executor.rs +++ b/integrations/wasi/src/executor.rs @@ -1,31 +1,46 @@ //! This is (Yet Another) Async Runtime for WASI with first-class support //! for `.await`-ing on [`Pollable`]. It is an ad-hoc implementation //! tailored for Leptos but it could be exported into a standalone crate. -//! +//! //! It is based on the `futures` crate's [`LocalPool`] and makes use of //! no `unsafe` code. -//! +//! //! # Performance Notes -//! +//! //! I haven't benchmarked this runtime but since it makes no use of unsafe code //! and Rust `core`'s `Context` was prematurely optimised for multi-threading //! environment, I had no choice but using synchronisation primitives to make //! the API happy. -//! +//! //! IIRC, `wasm32` targets have an implementation of synchronisation primitives //! that are just stubs, downgrading them to their single-threaded counterpart //! so the overhead should be minimal. -//! +//! //! Also, you can customise the behaviour of the [`Executor`] using the //! [`Mode`] enum to trade-off reactivity for less host context switch //! with the [`Mode::Stalled`] variant. -use std::{cell::RefCell, future::Future, mem, rc::Rc, sync::{Arc, OnceLock}, task::{Context, Poll, Wake, Waker}}; +use std::{ + cell::RefCell, + future::Future, + mem, + rc::Rc, + sync::{Arc, OnceLock}, + task::{Context, Poll, Wake, Waker}, +}; use any_spawner::CustomExecutor; -use futures::{channel::mpsc::{UnboundedReceiver, UnboundedSender}, executor::{LocalPool, LocalSpawner}, task::{LocalSpawnExt, SpawnExt}, FutureExt, Stream}; +use futures::{ + channel::mpsc::{UnboundedReceiver, UnboundedSender}, + executor::{LocalPool, LocalSpawner}, + task::{LocalSpawnExt, SpawnExt}, + FutureExt, Stream, +}; use parking_lot::Mutex; -use wasi::{clocks::monotonic_clock::{subscribe_duration, Duration}, io::poll::{poll, Pollable}}; +use wasi::{ + clocks::monotonic_clock::{subscribe_duration, Duration}, + io::poll::{poll, Pollable}, +}; struct TableEntry(Pollable, Waker); @@ -51,21 +66,22 @@ impl WaitPoll { impl Future for WaitPoll { type Output = (); - fn poll(self: std::pin::Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + fn poll( + self: std::pin::Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll { match &mut self.get_mut().0 { this @ WaitPollInner::Unregistered(_) => { let waker = Arc::new(WaitPollWaker::new(cx.waker())); if let Some(sender) = POLLABLE_SINK.get() { - if let WaitPollInner::Unregistered(pollable) = mem::replace(this, WaitPollInner::Registered(waker.clone())) { + if let WaitPollInner::Unregistered(pollable) = mem::replace( + this, + WaitPollInner::Registered(waker.clone()), + ) { sender .clone() - .unbounded_send( - TableEntry( - pollable, - waker.into(), - ) - ) + .unbounded_send(TableEntry(pollable, waker.into())) .expect("cannot spawn a new WaitPoll"); Poll::Pending @@ -73,9 +89,11 @@ impl Future for WaitPoll { unreachable!(); } } else { - panic!("cannot create a WaitPoll before creating an Executor"); + panic!( + "cannot create a WaitPoll before creating an Executor" + ); } - }, + } WaitPollInner::Registered(waker) => { let mut lock = waker.0.lock(); if lock.done { @@ -106,7 +124,7 @@ impl WaitPollWaker { fn new(waker: &Waker) -> Self { Self(Mutex::new(WaitPollWakerInner { done: false, - task_waker: waker.clone() + task_waker: waker.clone(), })) } } @@ -175,7 +193,9 @@ impl Executor { loop { match rx.try_recv() { - Err(_) => panic!("internal error: sender of run until has been dropped"), + Err(_) => panic!( + "internal error: sender of run until has been dropped" + ), Ok(Some(val)) => return val, Ok(None) => { self.poll_local(); @@ -202,7 +222,9 @@ impl CustomExecutor for Executor { }; match self.0.mode { - Mode::Premptive => { pool.try_run_one(); }, + Mode::Premptive => { + pool.try_run_one(); + } Mode::Stalled => pool.run_until_stalled(), }; @@ -210,13 +232,13 @@ impl CustomExecutor for Executor { let capacity = upper.unwrap_or(lower); let mut entries = Vec::with_capacity(capacity); let mut rx = self.0.rx.borrow_mut(); - + loop { match rx.try_next() { Ok(None) => break, Ok(Some(entry)) => { entries.push(Some(entry)); - }, + } Err(_) => break, } } @@ -238,7 +260,7 @@ impl CustomExecutor for Executor { if let Some(sender) = POLLABLE_SINK.get() { let sender = sender.clone(); - + // Wakes futures subscribed to ready pollable. for index in ready { let wake = entries[index as usize].take().unwrap().1; @@ -247,9 +269,9 @@ impl CustomExecutor for Executor { // Requeue not ready pollable. for entry in entries.into_iter().flatten() { - sender - .unbounded_send(entry) - .expect("the sender channel is closed"); + sender + .unbounded_send(entry) + .expect("the sender channel is closed"); } } else { unreachable!(); diff --git a/integrations/wasi/src/handler.rs b/integrations/wasi/src/handler.rs index 4cbfd5cf48..58be7a0c66 100644 --- a/integrations/wasi/src/handler.rs +++ b/integrations/wasi/src/handler.rs @@ -30,7 +30,7 @@ use leptos_router::{ use mime_guess::MimeGuess; use routefinder::Router; use server_fn::middleware::Service; -use throw_error::Error; +use thiserror::Error; use wasi::http::types::{ IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, @@ -119,7 +119,7 @@ impl Handler { pub fn build( req: IncomingRequest, res_out: ResponseOutparam, - ) -> Result { + ) -> Result { Ok(Self { req: crate::request::Request(req).try_into()?, res_out, @@ -310,7 +310,7 @@ impl Handler { self, app: impl Fn() -> IV + 'static + Send + Clone, additional_context: impl Fn() + 'static + Clone + Send, - ) -> Result<(), Error> + ) -> Result<(), HandlerError> where IV: IntoView + 'static, { @@ -342,7 +342,7 @@ impl Handler { let mut res = sfn.run(req).await; - // it it accepts text/html (i.e., is a plain form post) and doesn't already have a + // if it accepts text/html (i.e., is a plain form post) and doesn't already have a // Location set, then redirect to to Referer if accepts_html { if let Some(referrer) = referrer { @@ -355,11 +355,6 @@ impl Handler { } } - match res.body() { - ServerFnBody::Async(_) => println!("cant print stream"), - ServerFnBody::Sync(data) => println!("will send : {}", String::from_utf8_lossy(data)) - }; - Some(res.into()) } else if let Some(best_match) = best_match { let listing = best_match.handler(); @@ -452,16 +447,18 @@ impl Handler { }; while let Some(buf) = input_stream.next().await { - let buf = buf?; + let buf = buf.map_err(HandlerError::ResponseStream)?; let chunks = buf.chunks(CHUNK_BYTE_SIZE); for chunk in chunks { - // TODO: better error handling there. - output_stream.blocking_write_and_flush(chunk)?; + output_stream + .blocking_write_and_flush(chunk) + .map_err(HandlerError::from)?; } } drop(output_stream); - OutgoingBody::finish(body, None)?; + OutgoingBody::finish(body, None) + .map_err(HandlerError::WasiResponseBody)?; Ok(()) } @@ -507,3 +504,21 @@ impl RouterPathRepresentation for &[PathSegment] { path } } + +#[derive(Error, Debug)] +pub enum HandlerError { + #[error("error handling request")] + Request(#[from] crate::request::RequestError), + + #[error("error handling response")] + Response(#[from] crate::response::ResponseError), + + #[error("response stream emitted an error")] + ResponseStream(throw_error::Error), + + #[error("wasi stream failure")] + WasiStream(#[from] wasi::io::streams::StreamError), + + #[error("failed to finish response body")] + WasiResponseBody(wasi::http::types::ErrorCode), +} diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs index 4fa9ba2f0a..777a2dfaab 100644 --- a/integrations/wasi/src/lib.rs +++ b/integrations/wasi/src/lib.rs @@ -38,21 +38,21 @@ //! //! We are using the bindings provided by the `wasi` crate. +pub mod executor; pub mod handler; pub mod request; pub mod response; pub mod utils; -pub mod executor; #[allow(clippy::pub_use)] pub mod prelude { + pub use crate::executor::Executor as WasiExecutor; pub use crate::handler::Handler; pub use crate::response::Body; pub use crate::utils::redirect; pub use wasi::exports::wasi::http::incoming_handler::{ IncomingRequest, ResponseOutparam, }; - pub use crate::executor::Executor as WasiExecutor; } /// When working with streams, this crate will try to chunk bytes with diff --git a/integrations/wasi/src/request.rs b/integrations/wasi/src/request.rs index 30346fcbbb..3d10f250ce 100644 --- a/integrations/wasi/src/request.rs +++ b/integrations/wasi/src/request.rs @@ -1,6 +1,6 @@ use bytes::Bytes; use http::{uri::Parts, Uri}; -use throw_error::Error; +use thiserror::Error; use wasi::{ http::types::{IncomingBody, IncomingRequest, Method, Scheme}, @@ -12,7 +12,7 @@ use crate::CHUNK_BYTE_SIZE; pub struct Request(pub IncomingRequest); impl TryFrom for http::Request { - type Error = Error; + type Error = RequestError; fn try_from(req: Request) -> Result { let mut builder = http::Request::builder(); @@ -58,28 +58,36 @@ impl TryFrom for http::Request { http::uri::Authority::from_maybe_shared(aut.into_bytes()) }) .transpose() - .map_err(Error::from)?; + .map_err(http::Error::from)?; uri_parts.path_and_query = req .path_with_query() .map(|paq| { http::uri::PathAndQuery::from_maybe_shared(paq.into_bytes()) }) .transpose() - .map_err(Error::from)?; + .map_err(http::Error::from)?; drop(body_stream); IncomingBody::finish(incoming_body); builder .method(req_method) - .uri(Uri::from_parts(uri_parts).map_err(Error::from)?) + .uri(Uri::from_parts(uri_parts).map_err(http::Error::from)?) .body(Bytes::from(body_bytes)) - .map_err(Error::from) + .map_err(RequestError::from) } } -pub fn method_wasi_to_http( - value: Method, -) -> Result { +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum RequestError { + #[error("failed to convert wasi bindings to http types")] + Http(#[from] http::Error), + + #[error("error while processing wasi:http body stream")] + WasiIo(#[from] wasi::io::streams::StreamError), +} + +pub fn method_wasi_to_http(value: Method) -> Result { match value { Method::Connect => Ok(http::Method::CONNECT), Method::Delete => Ok(http::Method::DELETE), @@ -90,16 +98,19 @@ pub fn method_wasi_to_http( Method::Post => Ok(http::Method::POST), Method::Put => Ok(http::Method::PUT), Method::Trace => Ok(http::Method::TRACE), - Method::Other(mtd) => http::Method::from_bytes(mtd.as_bytes()), + Method::Other(mtd) => { + http::Method::from_bytes(mtd.as_bytes()).map_err(http::Error::from) + } } } pub fn scheme_wasi_to_http( value: Scheme, -) -> Result { +) -> Result { match value { Scheme::Http => Ok(http::uri::Scheme::HTTP), Scheme::Https => Ok(http::uri::Scheme::HTTPS), - Scheme::Other(oth) => http::uri::Scheme::try_from(oth.as_bytes()), + Scheme::Other(oth) => http::uri::Scheme::try_from(oth.as_bytes()) + .map_err(http::Error::from), } } diff --git a/integrations/wasi/src/response.rs b/integrations/wasi/src/response.rs index b222372712..605af7e28d 100644 --- a/integrations/wasi/src/response.rs +++ b/integrations/wasi/src/response.rs @@ -7,9 +7,9 @@ use leptos_integration_utils::ExtendResponse; use parking_lot::RwLock; use server_fn::response::generic::Body as ServerFnBody; -use throw_error::Error; +use thiserror::Error; -use wasi::http::types::Headers; +use wasi::http::types::{HeaderError, Headers}; /// This crate uses platform-agnostic [`http::Response`] /// with a custom [`Body`] and convert them under the hood to @@ -22,7 +22,7 @@ use wasi::http::types::Headers; pub struct Response(pub http::Response); impl Response { - pub fn headers(&self) -> Result { + pub fn headers(&self) -> Result { let headers = Headers::new(); for (name, value) in self.0.headers() { headers.append(&name.to_string(), &Vec::from(value.as_bytes()))?; @@ -47,7 +47,15 @@ pub enum Body { /// The response body will be written asynchronously, /// this execution model is also known as /// "streaming". - Async(Pin> + Send + 'static>>), + Async( + Pin< + Box< + dyn Stream> + + Send + + 'static, + >, + >, + ), } impl From for Body { @@ -100,8 +108,9 @@ impl ExtendResponse for Response { fn from_stream( stream: impl Stream + Send + 'static, ) -> Self { - let stream = - stream.map(|data| Result::::Ok(Bytes::from(data))); + let stream = stream.map(|data| { + Result::::Ok(Bytes::from(data)) + }); Self(http::Response::new(Body::Async(Box::pin(stream)))) } @@ -126,3 +135,10 @@ impl ExtendResponse for Response { } } } + +#[derive(Error, Debug)] +#[non_exhaustive] +pub enum ResponseError { + #[error("failed to parse http::Response's headers")] + WasiHeaders(#[from] HeaderError), +} diff --git a/integrations/wasi/src/utils.rs b/integrations/wasi/src/utils.rs index 06945803a2..66ca0ac77d 100644 --- a/integrations/wasi/src/utils.rs +++ b/integrations/wasi/src/utils.rs @@ -1,14 +1,10 @@ -use http::{ - header::{self, ACCEPT}, - request::Parts, - HeaderName, HeaderValue, StatusCode, -}; +use http::{header, request::Parts, HeaderName, HeaderValue, StatusCode}; use leptos::prelude::use_context; use server_fn::redirect::REDIRECT_HEADER; use crate::response::ResponseOptions; -// Allow the user to perform redirection easily. +/// Allow to return an HTTP redirection from components. pub fn redirect(path: &str) { if let (Some(req), Some(res)) = (use_context::(), use_context::()) @@ -22,7 +18,7 @@ pub fn redirect(path: &str) { let accepts_html = req .headers - .get(ACCEPT) + .get(header::ACCEPT) .and_then(|v| v.to_str().ok()) .map(|v| v.contains("text/html")) .unwrap_or(false); From 345b762c94be36d49a8a265c7b8db4edccaa5eda Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Thu, 24 Oct 2024 19:55:44 +0200 Subject: [PATCH 18/23] chore: migrate integration off-tree Signed-off-by: Enzo "raskyld" Nocera --- integrations/wasi/Cargo.toml | 34 -- integrations/wasi/Makefile.toml | 4 - integrations/wasi/src/executor.rs | 280 ---------------- integrations/wasi/src/handler.rs | 524 ------------------------------ integrations/wasi/src/lib.rs | 60 ---- integrations/wasi/src/request.rs | 116 ------- integrations/wasi/src/response.rs | 144 -------- integrations/wasi/src/utils.rs | 44 --- 8 files changed, 1206 deletions(-) delete mode 100644 integrations/wasi/Cargo.toml delete mode 100644 integrations/wasi/Makefile.toml delete mode 100644 integrations/wasi/src/executor.rs delete mode 100644 integrations/wasi/src/handler.rs delete mode 100644 integrations/wasi/src/lib.rs delete mode 100644 integrations/wasi/src/request.rs delete mode 100644 integrations/wasi/src/response.rs delete mode 100644 integrations/wasi/src/utils.rs diff --git a/integrations/wasi/Cargo.toml b/integrations/wasi/Cargo.toml deleted file mode 100644 index 2e97ad2f6f..0000000000 --- a/integrations/wasi/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "leptos_wasi" -authors = ["Enzo Nocera"] -license = "MIT" -repository = "https://github.com/leptos-rs/leptos" -description = "WASI integrations for the Leptos web framework." -version = { workspace = true } -rust-version.workspace = true -edition.workspace = true - -[dependencies] -any_spawner = { workspace = true, features = ["futures-executor"] } -throw_error = { workspace = true } -hydration_context = { workspace = true } -futures = "0.3.30" -wasi = "0.13.1+wasi-0.2.0" -leptos = { workspace = true, features = ["nonce", "ssr"] } -leptos_meta = { workspace = true, features = ["ssr"] } -leptos_router = { workspace = true, features = ["ssr"] } -leptos_macro = { workspace = true, features = ["generic"] } -leptos_integration_utils = { workspace = true } -server_fn = { workspace = true, features = ["generic"] } -http = "1.1.0" -parking_lot = "0.12.3" -bytes = "1.7.2" -routefinder = "0.5.4" -mime_guess = "2.0" -thiserror = "1.0.65" - -[features] -islands-router = [] - -[package.metadata.docs.rs] -rustdoc-args = ["--generate-link-to-definition"] diff --git a/integrations/wasi/Makefile.toml b/integrations/wasi/Makefile.toml deleted file mode 100644 index 4ed6229141..0000000000 --- a/integrations/wasi/Makefile.toml +++ /dev/null @@ -1,4 +0,0 @@ -extend = { path = "../../cargo-make/main.toml" } - -[tasks.check-format] -env = { LEPTOS_PROJECT_DIRECTORY = "../../" } diff --git a/integrations/wasi/src/executor.rs b/integrations/wasi/src/executor.rs deleted file mode 100644 index 8c604f83ab..0000000000 --- a/integrations/wasi/src/executor.rs +++ /dev/null @@ -1,280 +0,0 @@ -//! This is (Yet Another) Async Runtime for WASI with first-class support -//! for `.await`-ing on [`Pollable`]. It is an ad-hoc implementation -//! tailored for Leptos but it could be exported into a standalone crate. -//! -//! It is based on the `futures` crate's [`LocalPool`] and makes use of -//! no `unsafe` code. -//! -//! # Performance Notes -//! -//! I haven't benchmarked this runtime but since it makes no use of unsafe code -//! and Rust `core`'s `Context` was prematurely optimised for multi-threading -//! environment, I had no choice but using synchronisation primitives to make -//! the API happy. -//! -//! IIRC, `wasm32` targets have an implementation of synchronisation primitives -//! that are just stubs, downgrading them to their single-threaded counterpart -//! so the overhead should be minimal. -//! -//! Also, you can customise the behaviour of the [`Executor`] using the -//! [`Mode`] enum to trade-off reactivity for less host context switch -//! with the [`Mode::Stalled`] variant. - -use std::{ - cell::RefCell, - future::Future, - mem, - rc::Rc, - sync::{Arc, OnceLock}, - task::{Context, Poll, Wake, Waker}, -}; - -use any_spawner::CustomExecutor; -use futures::{ - channel::mpsc::{UnboundedReceiver, UnboundedSender}, - executor::{LocalPool, LocalSpawner}, - task::{LocalSpawnExt, SpawnExt}, - FutureExt, Stream, -}; -use parking_lot::Mutex; -use wasi::{ - clocks::monotonic_clock::{subscribe_duration, Duration}, - io::poll::{poll, Pollable}, -}; - -struct TableEntry(Pollable, Waker); - -static POLLABLE_SINK: OnceLock> = OnceLock::new(); - -pub async fn sleep(duration: Duration) { - WaitPoll::new(subscribe_duration(duration)).await -} - -pub struct WaitPoll(WaitPollInner); - -enum WaitPollInner { - Unregistered(Pollable), - Registered(Arc), -} - -impl WaitPoll { - pub fn new(pollable: Pollable) -> Self { - Self(WaitPollInner::Unregistered(pollable)) - } -} - -impl Future for WaitPoll { - type Output = (); - - fn poll( - self: std::pin::Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll { - match &mut self.get_mut().0 { - this @ WaitPollInner::Unregistered(_) => { - let waker = Arc::new(WaitPollWaker::new(cx.waker())); - - if let Some(sender) = POLLABLE_SINK.get() { - if let WaitPollInner::Unregistered(pollable) = mem::replace( - this, - WaitPollInner::Registered(waker.clone()), - ) { - sender - .clone() - .unbounded_send(TableEntry(pollable, waker.into())) - .expect("cannot spawn a new WaitPoll"); - - Poll::Pending - } else { - unreachable!(); - } - } else { - panic!( - "cannot create a WaitPoll before creating an Executor" - ); - } - } - WaitPollInner::Registered(waker) => { - let mut lock = waker.0.lock(); - if lock.done { - Poll::Ready(()) - } else { - // How can it happen?! :O - // Well, if, for some reason, the Task get woken up for - // another reason than the pollable associated with this - // WaitPoll got ready. - // - // We need to make sure we update the waker. - lock.task_waker = cx.waker().clone(); - Poll::Pending - } - } - } - } -} - -struct WaitPollWaker(Mutex); - -struct WaitPollWakerInner { - done: bool, - task_waker: Waker, -} - -impl WaitPollWaker { - fn new(waker: &Waker) -> Self { - Self(Mutex::new(WaitPollWakerInner { - done: false, - task_waker: waker.clone(), - })) - } -} - -impl Wake for WaitPollWaker { - fn wake(self: std::sync::Arc) { - self.wake_by_ref(); - } - - fn wake_by_ref(self: &std::sync::Arc) { - let mut lock = self.0.lock(); - lock.task_waker.wake_by_ref(); - lock.done = true; - } -} - -/// Controls how often the [`Executor`] checks for [`Pollable`] readiness. -pub enum Mode { - /// Will check as often as possible for readiness, this have some - /// performance overhead. - Premptive, - - /// Will only check for readiness when no more progress can be made - /// on pooled Futures. - Stalled, -} - -#[derive(Clone)] -pub struct Executor(Rc); - -struct ExecutorInner { - pool: RefCell, - spawner: LocalSpawner, - rx: RefCell>, - mode: Mode, -} - -impl Executor { - pub fn new(mode: Mode) -> Self { - let pool = LocalPool::new(); - let spawner = pool.spawner(); - let (tx, rx) = futures::channel::mpsc::unbounded(); - - POLLABLE_SINK - .set(tx.clone()) - .expect("calling Executor::new two times is not supported"); - - Self(Rc::new(ExecutorInner { - pool: RefCell::new(pool), - spawner, - rx: RefCell::new(rx), - mode, - })) - } - - pub fn run_until(&self, fut: T) -> T::Output - where - T: Future + 'static, - { - let (tx, mut rx) = futures::channel::oneshot::channel::(); - self.spawn_local(Box::pin(fut.then(|val| async move { - if tx.send(val).is_err() { - panic!("failed to send the return value of the future passed to run_until"); - } - }))); - - loop { - match rx.try_recv() { - Err(_) => panic!( - "internal error: sender of run until has been dropped" - ), - Ok(Some(val)) => return val, - Ok(None) => { - self.poll_local(); - } - } - } - } -} - -impl CustomExecutor for Executor { - fn spawn(&self, fut: any_spawner::PinnedFuture<()>) { - self.0.spawner.spawn(fut).unwrap(); - } - - fn spawn_local(&self, fut: any_spawner::PinnedLocalFuture<()>) { - self.0.spawner.spawn_local(fut).unwrap(); - } - - fn poll_local(&self) { - let mut pool = match self.0.pool.try_borrow_mut() { - Ok(pool) => pool, - // Nested call to poll_local(), noop. - Err(_) => return, - }; - - match self.0.mode { - Mode::Premptive => { - pool.try_run_one(); - } - Mode::Stalled => pool.run_until_stalled(), - }; - - let (lower, upper) = self.0.rx.borrow().size_hint(); - let capacity = upper.unwrap_or(lower); - let mut entries = Vec::with_capacity(capacity); - let mut rx = self.0.rx.borrow_mut(); - - loop { - match rx.try_next() { - Ok(None) => break, - Ok(Some(entry)) => { - entries.push(Some(entry)); - } - Err(_) => break, - } - } - - if entries.is_empty() { - // This could happen if some Futures use Waker that are not - // registered through [`WaitPoll`] or that we are blocked - // because some Future returned `Poll::Pending` without - // actually making sure their Waker is called at some point. - return; - } - - let pollables = entries - .iter() - .map(|entry| &entry.as_ref().unwrap().0) - .collect::>(); - - let ready = poll(&pollables); - - if let Some(sender) = POLLABLE_SINK.get() { - let sender = sender.clone(); - - // Wakes futures subscribed to ready pollable. - for index in ready { - let wake = entries[index as usize].take().unwrap().1; - wake.wake(); - } - - // Requeue not ready pollable. - for entry in entries.into_iter().flatten() { - sender - .unbounded_send(entry) - .expect("the sender channel is closed"); - } - } else { - unreachable!(); - } - } -} diff --git a/integrations/wasi/src/handler.rs b/integrations/wasi/src/handler.rs deleted file mode 100644 index 58be7a0c66..0000000000 --- a/integrations/wasi/src/handler.rs +++ /dev/null @@ -1,524 +0,0 @@ -#![forbid(unsafe_code)] - -use std::sync::Arc; - -use bytes::Bytes; -use futures::{ - stream::{self, once}, - StreamExt, -}; -use http::{ - header::{ACCEPT, LOCATION, REFERER}, - request::Parts, - HeaderValue, StatusCode, Uri, -}; -use hydration_context::SsrSharedContext; -use leptos::{ - prelude::{provide_context, Owner, ScopedFuture}, - server_fn::{ - codec::Encoding, http_export::Request, - response::generic::Body as ServerFnBody, ServerFn, ServerFnTraitObj, - }, - IntoView, -}; -use leptos_integration_utils::{ExtendResponse, PinnedStream}; -use leptos_meta::ServerMetaContext; -use leptos_router::{ - components::provide_server_redirect, location::RequestUrl, PathSegment, - RouteList, RouteListing, SsrMode, -}; -use mime_guess::MimeGuess; -use routefinder::Router; -use server_fn::middleware::Service; -use thiserror::Error; - -use wasi::http::types::{ - IncomingRequest, OutgoingBody, OutgoingResponse, ResponseOutparam, -}; - -use crate::{ - response::{Body, Response, ResponseOptions}, - utils::redirect, - CHUNK_BYTE_SIZE, -}; - -/// Handle routing, static file serving and response tx using the low-level -/// `wasi:http` APIs. -/// -/// ## Usage -/// -/// Please, note that the handler expect to be run with a local Executor initiated. -/// -/// ``` -/// use leptos_wasi::prelude::Handler; -/// -/// let conf = get_configuration(None).unwrap(); -/// let leptos_options = conf.leptos_options; -/// -/// Handler::build(request, response_out) -/// .expect("could not create handler") -/// // Those two functions should be called first because they can -/// // *shortcut* the handler, see "Performance Considerations". -/// -/// // Any HTTP request prefixed with `/pkg` will call the passed -/// // `serve_static_files` function to deliver static files. -/// .static_files_handler("/pkg", serve_static_files) -/// .with_server_fn::() -/// // Fetch all available routes from your App. -/// .generate_routes(App) -/// // Actually process the request and write the response. -/// .handle_with_context(move || shell(leptos_options.clone()), || {}).await.expect("could not handle the request"); -/// ``` -/// -/// ## Performance Considerations -/// -/// This handler is optimised for the special case of WASI Components being spawned -/// on a per-request basis. That is, the lifetime of the component is bound to the -/// one of the request, so we don't do any fancy pre-setup: it means -/// **your Server-Side will always be cold-started**. -/// -/// While it could have a bad impact on the performance of your app, please, know -/// that there is a *shotcut* mechanism implemented that allows the [`Handler`] -/// to shortcut the whole HTTP Rendering and Reactivity logic to directly jump to -/// writting the response in those case: -/// -/// * The user request a static-file, then, calling [`Handler::static_files_handler`] -/// will *shortcut* the handler and all future calls are ignored to reach -/// [`Handler::handle_with_context`] *almost* instantly. -/// * The user reach a server function, then, calling [`Handler::with_server_fn`] -/// will check if the request's path matches the one from the passed server functions, -/// if so, *shortcut* the handler. -/// -/// This implementation ensures that, even though your component is cold-started -/// on each request, the performance are good. Please, note that this approach is -/// directly enabled by the fact WASI Components have under-millisecond start-up -/// times! It wouldn't be practical to do that with traditional container-based solutions. -/// -/// ## Limitations -/// -/// [`SsrMode::Static`] is not implemented yet, having one in your `` -/// will cause [`Handler::handle_with_context`] to panic! -pub struct Handler { - req: Request, - res_out: ResponseOutparam, - - // *shortcut* if any is set - server_fn: - Option, http::Response>>, - preset_res: Option, - should_404: bool, - - // built using the user-defined app_fn - ssr_router: Router, -} - -impl Handler { - /// Wraps the WASI resources to handle the request. - /// Could fail if the [`IncomingRequest`] cannot be converted to - /// a [`http:Request`]. - pub fn build( - req: IncomingRequest, - res_out: ResponseOutparam, - ) -> Result { - Ok(Self { - req: crate::request::Request(req).try_into()?, - res_out, - server_fn: None, - preset_res: None, - ssr_router: Router::new(), - should_404: false, - }) - } - - // Test whether we are ready to send a response to shortcut some - // code and provide a fast-path. - #[inline] - const fn shortcut(&self) -> bool { - self.server_fn.is_some() || self.preset_res.is_some() || self.should_404 - } - - /// Tests if the request path matches the bound server function - /// and *shortcut* the [`Handler`] to quickly reach - /// the call to [`Handler::handle_with_context`]. - pub fn with_server_fn(mut self) -> Self - where - T: ServerFn< - ServerRequest = Request, - ServerResponse = http::Response, - > + 'static, - { - if self.shortcut() { - return self; - } - - if self.req.method() == T::InputEncoding::METHOD - && self.req.uri().path() == T::PATH - { - self.server_fn = Some(ServerFnTraitObj::new( - T::PATH, - T::InputEncoding::METHOD, - |request| Box::pin(T::run_on_server(request)), - T::middlewares, - )); - } - - self - } - - /// If the request is prefixed with `prefix` [`Uri`], then - /// the handler will call the passed `handler` with the Uri trimmed of - /// the prefix. If the closure returns - /// None, the response will be 404, otherwise, the returned [`Body`] - /// will be served as-if. - /// - /// This function, when matching, *shortcut* the [`Handler`] to quickly reach - /// the call to [`Handler::handle_with_context`]. - pub fn static_files_handler( - mut self, - prefix: T, - handler: impl Fn(String) -> Option + 'static + Send + Clone, - ) -> Self - where - T: TryInto, - >::Error: std::error::Error, - { - if self.shortcut() { - return self; - } - - if let Some(trimmed_url) = self.req.uri().path().strip_prefix( - prefix.try_into().expect("you passed an invalid Uri").path(), - ) { - match handler(trimmed_url.to_string()) { - None => self.should_404 = true, - Some(body) => { - let mut res = http::Response::new(body); - let mime = MimeGuess::from_path(trimmed_url); - - res.headers_mut().insert( - http::header::CONTENT_TYPE, - HeaderValue::from_str( - mime.first_or_octet_stream().as_ref(), - ) - .expect("internal error: could not parse MIME type"), - ); - - self.preset_res = Some(Response(res)); - } - } - } - - self - } - - /// This mocks a request to the `app_fn` component to extract your - /// ``'s ``. - pub fn generate_routes( - self, - app_fn: impl Fn() -> IV + 'static + Send + Clone, - ) -> Self - where - IV: IntoView + 'static, - { - self.generate_routes_with_exclusions_and_context(app_fn, None, || {}) - } - - /// This mocks a request to the `app_fn` component to extract your - /// ``'s ``. - /// - /// You can pass an `additional_context` to [`provide_context`] to the - /// application. - pub fn generate_routes_with_context( - self, - app_fn: impl Fn() -> IV + 'static + Send + Clone, - additional_context: impl Fn() + 'static + Send + Clone, - ) -> Self - where - IV: IntoView + 'static, - { - self.generate_routes_with_exclusions_and_context( - app_fn, - None, - additional_context, - ) - } - - /// This mocks a request to the `app_fn` component to extract your - /// ``'s ``. - /// - /// You can pass an `additional_context` to [`provide_context`] to the - /// application. - /// - /// You can pass a list of `excluded_routes` to avoid generating them. - pub fn generate_routes_with_exclusions_and_context( - mut self, - app_fn: impl Fn() -> IV + 'static + Send + Clone, - excluded_routes: Option>, - additional_context: impl Fn() + 'static + Send + Clone, - ) -> Self - where - IV: IntoView + 'static, - { - // If we matched a server function, we do not need to go through - // all of that. - if self.shortcut() { - return self; - } - - if !self.ssr_router.is_empty() { - panic!("generate_routes was called twice"); - } - - let owner = Owner::new_root(Some(Arc::new(SsrSharedContext::new()))); - let (mock_meta, _) = ServerMetaContext::new(); - let routes = owner - .with(|| { - // as we are generating the app to extract - // the , we want to mock the root path. - provide_context(RequestUrl::new("")); - provide_context(ResponseOptions::default()); - provide_context(http::uri::Parts::default()); - provide_context(mock_meta); - additional_context(); - RouteList::generate(&app_fn) - }) - .unwrap_or_default() - .into_inner() - .into_iter() - .map(|rt| (rt.path().to_rf_str_representation(), rt)) - .filter(|route| { - excluded_routes.as_ref().map_or(true, |excluded_routes| { - !excluded_routes.iter().any(|ex_path| *ex_path == route.0) - }) - }); - - for (path, route_listing) in routes { - self.ssr_router - .add(path, route_listing) - .expect("internal error: impossible to parse a RouteListing"); - } - - self - } - - /// Consumes the [`Handler`] to actually perform all the request handling - /// logic. - /// - /// You can pass an `additional_context` to [`provide_context`] to the - /// application. - pub async fn handle_with_context( - self, - app: impl Fn() -> IV + 'static + Send + Clone, - additional_context: impl Fn() + 'static + Clone + Send, - ) -> Result<(), HandlerError> - where - IV: IntoView + 'static, - { - let path = self.req.uri().path().to_string(); - let best_match = self.ssr_router.best_match(&path); - let (parts, body) = self.req.into_parts(); - let context_parts = parts.clone(); - let req = Request::from_parts(parts, body); - - let owner = Owner::new(); - let response = owner.with(|| { - ScopedFuture::new(async move { - let res_opts = ResponseOptions::default(); - let response: Option = if self.should_404 { - None - } else if self.preset_res.is_some() { - self.preset_res - } else if let Some(mut sfn) = self.server_fn { - provide_contexts(additional_context, context_parts, res_opts.clone()); - - // store Accepts and Referer in case we need them for redirect (below) - let accepts_html = req - .headers() - .get(ACCEPT) - .and_then(|v| v.to_str().ok()) - .map(|v| v.contains("text/html")) - .unwrap_or(false); - let referrer = req.headers().get(REFERER).cloned(); - - let mut res = sfn.run(req).await; - - // if it accepts text/html (i.e., is a plain form post) and doesn't already have a - // Location set, then redirect to to Referer - if accepts_html { - if let Some(referrer) = referrer { - let has_location = - res.headers().get(LOCATION).is_some(); - if !has_location { - *res.status_mut() = StatusCode::FOUND; - res.headers_mut().insert(LOCATION, referrer); - } - } - } - - Some(res.into()) - } else if let Some(best_match) = best_match { - let listing = best_match.handler(); - let (meta_context, meta_output) = ServerMetaContext::new(); - - let add_ctx = additional_context.clone(); - let additional_context = { - let res_opts = res_opts.clone(); - let meta_ctx = meta_context.clone(); - move || { - provide_contexts(add_ctx, context_parts, res_opts); - provide_context(meta_ctx); - } - }; - - Some(Response::from_app( - app, - meta_output, - additional_context, - res_opts.clone(), - match listing.mode() { - SsrMode::Async => |app, chunks| { - Box::pin(async move { - let app = if cfg!(feature = "islands-router") { - app.to_html_stream_in_order_branching() - } else { - app.to_html_stream_in_order() - }; - let app = app.collect::().await; - let chunks = chunks(); - Box::pin(once(async move { app }).chain(chunks)) as PinnedStream - }) - }, - SsrMode::InOrder => |app, chunks| { - Box::pin(async move { - let app = if cfg!(feature = "islands-router") { - app.to_html_stream_in_order_branching() - } else { - app.to_html_stream_in_order() - }; - Box::pin(app.chain(chunks())) as PinnedStream - }) - }, - SsrMode::PartiallyBlocked | SsrMode::OutOfOrder => |app, chunks| { - Box::pin(async move { - let app = if cfg!(feature = "islands-router") { - app.to_html_stream_out_of_order_branching() - } else { - app.to_html_stream_out_of_order() - }; - Box::pin(app.chain(chunks())) as PinnedStream - }) - }, - SsrMode::Static(_) => panic!("SsrMode::Static routes are not supported yet!") - } - ).await) - } else { - None - }; - - response.map(|mut req| { - req.extend_response(&res_opts); - req - }) - }) - }).await; - - let response = response.unwrap_or_else(|| { - let body = Bytes::from("404 not found"); - let mut res = http::Response::new(Body::Sync(body)); - *res.status_mut() = http::StatusCode::NOT_FOUND; - Response(res) - }); - - let headers = response.headers()?; - let wasi_res = OutgoingResponse::new(headers); - - wasi_res - .set_status_code(response.0.status().as_u16()) - .expect("invalid http status code was returned"); - let body = wasi_res.body().expect("unable to take response body"); - ResponseOutparam::set(self.res_out, Ok(wasi_res)); - - let output_stream = body - .write() - .expect("unable to open writable stream on body"); - let mut input_stream = match response.0.into_body() { - Body::Sync(buf) => Box::pin(stream::once(async { Ok(buf) })), - Body::Async(stream) => stream, - }; - - while let Some(buf) = input_stream.next().await { - let buf = buf.map_err(HandlerError::ResponseStream)?; - let chunks = buf.chunks(CHUNK_BYTE_SIZE); - for chunk in chunks { - output_stream - .blocking_write_and_flush(chunk) - .map_err(HandlerError::from)?; - } - } - - drop(output_stream); - OutgoingBody::finish(body, None) - .map_err(HandlerError::WasiResponseBody)?; - - Ok(()) - } -} - -fn provide_contexts( - additional_context: impl Fn() + 'static + Clone + Send, - context_parts: Parts, - res_opts: ResponseOptions, -) { - additional_context(); - provide_context(RequestUrl::new(context_parts.uri.path())); - provide_context(context_parts); - provide_context(res_opts); - provide_server_redirect(redirect); -} - -trait RouterPathRepresentation { - fn to_rf_str_representation(&self) -> String; -} - -impl RouterPathRepresentation for &[PathSegment] { - fn to_rf_str_representation(&self) -> String { - let mut path = String::new(); - for segment in self.iter() { - // TODO trailing slash handling - let raw = segment.as_raw_str(); - if !raw.is_empty() && !raw.starts_with('/') { - path.push('/'); - } - match segment { - PathSegment::Static(s) => path.push_str(s), - PathSegment::Param(s) => { - path.push(':'); - path.push_str(s); - } - PathSegment::Splat(_) => { - path.push('*'); - } - PathSegment::Unit => {} - } - } - path - } -} - -#[derive(Error, Debug)] -pub enum HandlerError { - #[error("error handling request")] - Request(#[from] crate::request::RequestError), - - #[error("error handling response")] - Response(#[from] crate::response::ResponseError), - - #[error("response stream emitted an error")] - ResponseStream(throw_error::Error), - - #[error("wasi stream failure")] - WasiStream(#[from] wasi::io::streams::StreamError), - - #[error("failed to finish response body")] - WasiResponseBody(wasi::http::types::ErrorCode), -} diff --git a/integrations/wasi/src/lib.rs b/integrations/wasi/src/lib.rs deleted file mode 100644 index 777a2dfaab..0000000000 --- a/integrations/wasi/src/lib.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! A first-party support of the `wasm32-wasip1` target for the **Server-Side** -//! of Leptos using the [`wasi:http`][wasi-http] proposal. -//! -//! [wasi-http]: https://github.com/WebAssembly/wasi-http -//! -//! # `Handler` -//! -//! The [`prelude::Handler`] is the main abstraction you will use. -//! -//! It expects being run in the context of a Future Executor `Task`, -//! since WASI is, at the moment, a single-threaded environment, -//! we provide a simple abstraction in the form of [`leptos::spawn::Executor`] -//! that you can leverage to use this crate. -//! -//! ``` -//! use wasi::exports::http::incoming_handler::*; -//! -//! struct LeptosServer; -//! -//! // NB(raskyld): for now, the types to use for the HTTP handlers are the one from -//! // the `leptos_wasi` crate, not the one generated in your crate. -//! impl Guest for LeptosServer { -//! fn handle(request: IncomingRequest, response_out: ResponseOutparam) { -//! // Initiate a single-threaded [`Future`] Executor so we can run the -//! // rendering system and take advantage of bodies streaming. -//! Executor::init_futures_local_executor().expect("cannot init future executor"); -//! Executor::spawn(async { -//! // declare an async function called `handle_request` and -//! // use the Handler in this function. -//! handle_request(request, response_out).await; -//! }); -//! Executor::run(); -//! } -//! } -//! ``` -//! -//! # WASI Bindings -//! -//! We are using the bindings provided by the `wasi` crate. - -pub mod executor; -pub mod handler; -pub mod request; -pub mod response; -pub mod utils; - -#[allow(clippy::pub_use)] -pub mod prelude { - pub use crate::executor::Executor as WasiExecutor; - pub use crate::handler::Handler; - pub use crate::response::Body; - pub use crate::utils::redirect; - pub use wasi::exports::wasi::http::incoming_handler::{ - IncomingRequest, ResponseOutparam, - }; -} - -/// When working with streams, this crate will try to chunk bytes with -/// this size. -const CHUNK_BYTE_SIZE: usize = 64; diff --git a/integrations/wasi/src/request.rs b/integrations/wasi/src/request.rs deleted file mode 100644 index 3d10f250ce..0000000000 --- a/integrations/wasi/src/request.rs +++ /dev/null @@ -1,116 +0,0 @@ -use bytes::Bytes; -use http::{uri::Parts, Uri}; -use thiserror::Error; - -use wasi::{ - http::types::{IncomingBody, IncomingRequest, Method, Scheme}, - io::streams::StreamError, -}; - -use crate::CHUNK_BYTE_SIZE; - -pub struct Request(pub IncomingRequest); - -impl TryFrom for http::Request { - type Error = RequestError; - - fn try_from(req: Request) -> Result { - let mut builder = http::Request::builder(); - let req = req.0; - let req_method = method_wasi_to_http(req.method())?; - let headers = req.headers(); - - for (header_name, header_value) in headers.entries() { - builder = builder.header(header_name, header_value); - } - - drop(headers); - - // NB(raskyld): consume could fail if, for some reason the caller - // manage to recreate an IncomingRequest backed by the same underlying - // resource handle (need to dig more to see if that's possible) - let incoming_body = req.consume().expect("could not consume body"); - - let body_stream = incoming_body - .stream() - .expect("could not create a stream from body"); - - let mut body_bytes = Vec::::with_capacity(CHUNK_BYTE_SIZE); - - loop { - match body_stream.blocking_read(CHUNK_BYTE_SIZE as u64) { - Err(StreamError::Closed) => break, - Err(StreamError::LastOperationFailed(err)) => { - return Err(StreamError::LastOperationFailed(err).into()) - } - Ok(data) => { - body_bytes.extend(data); - } - } - } - - let mut uri_parts = Parts::default(); - - uri_parts.scheme = req.scheme().map(scheme_wasi_to_http).transpose()?; - uri_parts.authority = req - .authority() - .map(|aut| { - http::uri::Authority::from_maybe_shared(aut.into_bytes()) - }) - .transpose() - .map_err(http::Error::from)?; - uri_parts.path_and_query = req - .path_with_query() - .map(|paq| { - http::uri::PathAndQuery::from_maybe_shared(paq.into_bytes()) - }) - .transpose() - .map_err(http::Error::from)?; - - drop(body_stream); - IncomingBody::finish(incoming_body); - builder - .method(req_method) - .uri(Uri::from_parts(uri_parts).map_err(http::Error::from)?) - .body(Bytes::from(body_bytes)) - .map_err(RequestError::from) - } -} - -#[derive(Error, Debug)] -#[non_exhaustive] -pub enum RequestError { - #[error("failed to convert wasi bindings to http types")] - Http(#[from] http::Error), - - #[error("error while processing wasi:http body stream")] - WasiIo(#[from] wasi::io::streams::StreamError), -} - -pub fn method_wasi_to_http(value: Method) -> Result { - match value { - Method::Connect => Ok(http::Method::CONNECT), - Method::Delete => Ok(http::Method::DELETE), - Method::Get => Ok(http::Method::GET), - Method::Head => Ok(http::Method::HEAD), - Method::Options => Ok(http::Method::OPTIONS), - Method::Patch => Ok(http::Method::PATCH), - Method::Post => Ok(http::Method::POST), - Method::Put => Ok(http::Method::PUT), - Method::Trace => Ok(http::Method::TRACE), - Method::Other(mtd) => { - http::Method::from_bytes(mtd.as_bytes()).map_err(http::Error::from) - } - } -} - -pub fn scheme_wasi_to_http( - value: Scheme, -) -> Result { - match value { - Scheme::Http => Ok(http::uri::Scheme::HTTP), - Scheme::Https => Ok(http::uri::Scheme::HTTPS), - Scheme::Other(oth) => http::uri::Scheme::try_from(oth.as_bytes()) - .map_err(http::Error::from), - } -} diff --git a/integrations/wasi/src/response.rs b/integrations/wasi/src/response.rs deleted file mode 100644 index 605af7e28d..0000000000 --- a/integrations/wasi/src/response.rs +++ /dev/null @@ -1,144 +0,0 @@ -use std::{pin::Pin, sync::Arc}; - -use bytes::Bytes; -use futures::{Stream, StreamExt}; -use http::{HeaderMap, HeaderName, HeaderValue, StatusCode}; -use leptos_integration_utils::ExtendResponse; -use parking_lot::RwLock; - -use server_fn::response::generic::Body as ServerFnBody; -use thiserror::Error; - -use wasi::http::types::{HeaderError, Headers}; - -/// This crate uses platform-agnostic [`http::Response`] -/// with a custom [`Body`] and convert them under the hood to -/// WASI native types. -/// -/// It supports both [`Body::Sync`] and [`Body::Async`], -/// allowing you to choose between synchronous response -/// (i.e. sending the whole response) and asynchronous response -/// (i.e. streaming the response). -pub struct Response(pub http::Response); - -impl Response { - pub fn headers(&self) -> Result { - let headers = Headers::new(); - for (name, value) in self.0.headers() { - headers.append(&name.to_string(), &Vec::from(value.as_bytes()))?; - } - Ok(headers) - } -} - -impl From> for Response -where - T: Into, -{ - fn from(value: http::Response) -> Self { - Self(value.map(Into::into)) - } -} - -pub enum Body { - /// The response body will be written synchronously. - Sync(Bytes), - - /// The response body will be written asynchronously, - /// this execution model is also known as - /// "streaming". - Async( - Pin< - Box< - dyn Stream> - + Send - + 'static, - >, - >, - ), -} - -impl From for Body { - fn from(value: ServerFnBody) -> Self { - match value { - ServerFnBody::Sync(data) => Self::Sync(data), - ServerFnBody::Async(stream) => Self::Async(stream), - } - } -} - -/// This struct lets you define headers and override the status of the Response from an Element or a Server Function -/// Typically contained inside of a ResponseOptions. Setting this is useful for cookies and custom responses. -#[derive(Debug, Clone, Default)] -pub struct ResponseParts { - pub headers: HeaderMap, - pub status: Option, -} - -/// Allows you to override details of the HTTP response like the status code and add Headers/Cookies. -#[derive(Debug, Clone, Default)] -pub struct ResponseOptions(Arc>); - -impl ResponseOptions { - /// A simpler way to overwrite the contents of `ResponseOptions` with a new `ResponseParts`. - #[inline] - pub fn overwrite(&self, parts: ResponseParts) { - *self.0.write() = parts - } - /// Set the status of the returned Response. - #[inline] - pub fn set_status(&self, status: StatusCode) { - self.0.write().status = Some(status); - } - /// Insert a header, overwriting any previous value with the same key. - #[inline] - pub fn insert_header(&self, key: HeaderName, value: HeaderValue) { - self.0.write().headers.insert(key, value); - } - /// Append a header, leaving any header with the same key intact. - #[inline] - pub fn append_header(&self, key: HeaderName, value: HeaderValue) { - self.0.write().headers.append(key, value); - } -} - -impl ExtendResponse for Response { - type ResponseOptions = ResponseOptions; - - fn from_stream( - stream: impl Stream + Send + 'static, - ) -> Self { - let stream = stream.map(|data| { - Result::::Ok(Bytes::from(data)) - }); - - Self(http::Response::new(Body::Async(Box::pin(stream)))) - } - - fn extend_response(&mut self, opt: &Self::ResponseOptions) { - let mut opt = opt.0.write(); - if let Some(status_code) = opt.status { - *self.0.status_mut() = status_code; - } - self.0 - .headers_mut() - .extend(std::mem::take(&mut opt.headers)); - } - - fn set_default_content_type(&mut self, content_type: &str) { - let headers = self.0.headers_mut(); - if !headers.contains_key(http::header::CONTENT_TYPE) { - headers.insert( - http::header::CONTENT_TYPE, - HeaderValue::from_str(content_type).unwrap(), - ); - } - } -} - -#[derive(Error, Debug)] -#[non_exhaustive] -pub enum ResponseError { - #[error("failed to parse http::Response's headers")] - WasiHeaders(#[from] HeaderError), -} diff --git a/integrations/wasi/src/utils.rs b/integrations/wasi/src/utils.rs deleted file mode 100644 index 66ca0ac77d..0000000000 --- a/integrations/wasi/src/utils.rs +++ /dev/null @@ -1,44 +0,0 @@ -use http::{header, request::Parts, HeaderName, HeaderValue, StatusCode}; -use leptos::prelude::use_context; -use server_fn::redirect::REDIRECT_HEADER; - -use crate::response::ResponseOptions; - -/// Allow to return an HTTP redirection from components. -pub fn redirect(path: &str) { - if let (Some(req), Some(res)) = - (use_context::(), use_context::()) - { - // insert the Location header in any case - res.insert_header( - header::LOCATION, - header::HeaderValue::from_str(path) - .expect("Failed to create HeaderValue"), - ); - - let accepts_html = req - .headers - .get(header::ACCEPT) - .and_then(|v| v.to_str().ok()) - .map(|v| v.contains("text/html")) - .unwrap_or(false); - if accepts_html { - // if the request accepts text/html, it's a plain form request and needs - // to have the 302 code set - res.set_status(StatusCode::FOUND); - } else { - // otherwise, we sent it from the server fn client and actually don't want - // to set a real redirect, as this will break the ability to return data - // instead, set the REDIRECT_HEADER to indicate that the client should redirect - res.insert_header( - HeaderName::from_static(REDIRECT_HEADER), - HeaderValue::from_str("").unwrap(), - ); - } - } else { - eprintln!( - "Couldn't retrieve either Parts or ResponseOptions while \ - trying to redirect()." - ); - } -} From 334d1dacdd62fbd5034a25b067ecdc53fa3ac7b4 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Thu, 24 Oct 2024 19:58:14 +0200 Subject: [PATCH 19/23] chore(ci): fix formatting Signed-off-by: Enzo "raskyld" Nocera --- any_spawner/src/lib.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/any_spawner/src/lib.rs b/any_spawner/src/lib.rs index 97a61ba714..25128f765c 100644 --- a/any_spawner/src/lib.rs +++ b/any_spawner/src/lib.rs @@ -293,7 +293,8 @@ impl Executor { pub fn init_custom_executor( custom_executor: impl CustomExecutor + Send + Sync + 'static, ) -> Result<(), ExecutorError> { - static EXECUTOR: OnceLock> = OnceLock::new(); + static EXECUTOR: OnceLock> = + OnceLock::new(); EXECUTOR .set(Box::new(custom_executor)) .map_err(|_| ExecutorError::AlreadySet)?; @@ -323,30 +324,23 @@ impl Executor { static EXECUTOR: OnceLock> = OnceLock::new(); } EXECUTOR.with(|this| { - this - .set(Box::new(custom_executor)) + this.set(Box::new(custom_executor)) .map_err(|_| ExecutorError::AlreadySet) })?; SPAWN .set(|fut| { - EXECUTOR.with(|this| { - this.get().unwrap().spawn(fut) - }); + EXECUTOR.with(|this| this.get().unwrap().spawn(fut)); }) .map_err(|_| ExecutorError::AlreadySet)?; SPAWN_LOCAL .set(|fut| { - EXECUTOR.with(|this| { - this.get().unwrap().spawn_local(fut) - }); + EXECUTOR.with(|this| this.get().unwrap().spawn_local(fut)); }) .map_err(|_| ExecutorError::AlreadySet)?; POLL_LOCAL .set(|| { - EXECUTOR.with(|this| { - this.get().unwrap().poll_local() - }); + EXECUTOR.with(|this| this.get().unwrap().poll_local()); }) .map_err(|_| ExecutorError::AlreadySet)?; Ok(()) From c9098024facaaf812cee61f2c6ec73ef0856e8f9 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Thu, 24 Oct 2024 19:59:28 +0200 Subject: [PATCH 20/23] chore: remove ref to leptos_wasi in Cargo.toml Signed-off-by: Enzo "raskyld" Nocera --- Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7ad80314f5..2647d15a07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,6 @@ members = [ "integrations/actix", "integrations/axum", "integrations/utils", - "integrations/wasi", # libraries "meta", From 19d53f0ac39396dc678f198498f9be2971f95ba2 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Thu, 31 Oct 2024 20:57:42 +0100 Subject: [PATCH 21/23] chore(ci): fix fmt Signed-off-by: Enzo "raskyld" Nocera --- server_fn/src/lib.rs | 3 --- server_fn/src/request/generic.rs | 6 ++---- server_fn_macro/src/lib.rs | 13 +++++++------ 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/server_fn/src/lib.rs b/server_fn/src/lib.rs index 6d960b4a28..7e91e1a114 100644 --- a/server_fn/src/lib.rs +++ b/server_fn/src/lib.rs @@ -117,18 +117,15 @@ pub mod response; #[cfg(feature = "actix")] #[doc(hidden)] pub use ::actix_web as actix_export; - #[cfg(feature = "axum-no-default")] #[doc(hidden)] pub use ::axum as axum_export; - #[cfg(feature = "generic")] #[doc(hidden)] pub use ::bytes as bytes_export; #[cfg(feature = "generic")] #[doc(hidden)] pub use ::http as http_export; - use client::Client; use codec::{Encoding, FromReq, FromRes, IntoReq, IntoRes}; #[doc(hidden)] diff --git a/server_fn/src/request/generic.rs b/server_fn/src/request/generic.rs index 98617f473a..8063bde5a8 100644 --- a/server_fn/src/request/generic.rs +++ b/server_fn/src/request/generic.rs @@ -12,16 +12,14 @@ //! * `wasm32-wasip*` integration crate `leptos_wasi` is using this //! crate under the hood. -use std::borrow::Cow; - +use crate::request::Req; use bytes::Bytes; use futures::{ stream::{self, Stream}, StreamExt, }; use http::Request; - -use crate::request::Req; +use std::borrow::Cow; impl Req for Request where diff --git a/server_fn_macro/src/lib.rs b/server_fn_macro/src/lib.rs index ada7c4a019..1e80222a8e 100644 --- a/server_fn_macro/src/lib.rs +++ b/server_fn_macro/src/lib.rs @@ -59,14 +59,15 @@ pub fn server_macro_impl( .inputs .iter_mut() .map(|f| { - let typed_arg = - match f { - FnArg::Receiver(_) => return Err(syn::Error::new( + let typed_arg = match f { + FnArg::Receiver(_) => { + return Err(syn::Error::new( f.span(), "cannot use receiver types in server function macro", - )), - FnArg::Typed(t) => t, - }; + )) + } + FnArg::Typed(t) => t, + }; // strip `mut`, which is allowed in fn args but not in struct fields if let Pat::Ident(ident) = &mut *typed_arg.pat { From 65078913cce52faf8177654e4da730165e68e656 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Thu, 31 Oct 2024 23:10:42 +0100 Subject: [PATCH 22/23] chore(ci): remove explicit into_inter() Signed-off-by: Enzo "raskyld" Nocera --- server_fn/src/request/generic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server_fn/src/request/generic.rs b/server_fn/src/request/generic.rs index 8063bde5a8..da1add07ff 100644 --- a/server_fn/src/request/generic.rs +++ b/server_fn/src/request/generic.rs @@ -45,7 +45,7 @@ where impl Stream> + Send + 'static, crate::ServerFnError, > { - Ok(stream::iter(self.into_body().into_iter()) + Ok(stream::iter(self.into_body()) .ready_chunks(16) .map(|chunk| Ok(Bytes::from(chunk)))) } From 5b51cd38dc482bef417395a949bc492debfa6533 Mon Sep 17 00:00:00 2001 From: "Enzo \"raskyld\" Nocera" Date: Fri, 1 Nov 2024 11:07:23 +0100 Subject: [PATCH 23/23] chore(ci): make generic mutually exclusive with other options Signed-off-by: Enzo "raskyld" Nocera --- leptos_macro/Cargo.toml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/leptos_macro/Cargo.toml b/leptos_macro/Cargo.toml index 556e7ce550..cc9e37a357 100644 --- a/leptos_macro/Cargo.toml +++ b/leptos_macro/Cargo.toml @@ -69,6 +69,14 @@ skip_feature_sets = [ "actix", "axum", ], + [ + "actix", + "generic", + ], + [ + "generic", + "axum", + ], ] [package.metadata.docs.rs]