From 4028e0ca1f3a4c13bdc0896a4826117eb79609a8 Mon Sep 17 00:00:00 2001 From: Tomasz Andrzejak Date: Mon, 16 Feb 2026 15:22:47 +0100 Subject: [PATCH] feat(p3-shim): sync with latest 0.3.0 draft --- crates/xtask/src/generate/wasi_types.rs | 2 +- .../test/fixtures/p3/wit/deps/cli/command.wit | 10 - .../fixtures/p3/wit/deps/cli/environment.wit | 22 - .../test/fixtures/p3/wit/deps/cli/exit.wit | 17 - .../test/fixtures/p3/wit/deps/cli/imports.wit | 34 - .../jco/test/fixtures/p3/wit/deps/cli/run.wit | 6 - .../test/fixtures/p3/wit/deps/cli/stdio.wit | 17 - .../fixtures/p3/wit/deps/cli/terminal.wit | 62 -- .../p3/wit/deps/clocks/monotonic-clock.wit | 45 -- .../fixtures/p3/wit/deps/clocks/timezone.wit | 55 -- .../p3/wit/deps/clocks/wall-clock.wit | 46 -- .../fixtures/p3/wit/deps/clocks/world.wit | 11 - .../p3/wit/deps/filesystem/preopens.wit | 11 - .../fixtures/p3/wit/deps/filesystem/types.wit | 629 --------------- .../fixtures/p3/wit/deps/filesystem/world.wit | 9 - .../fixtures/p3/wit/deps/http/handler.wit | 17 - .../test/fixtures/p3/wit/deps/http/proxy.wit | 44 - .../p3/wit/deps/random/insecure-seed.wit | 27 - .../fixtures/p3/wit/deps/random/insecure.wit | 25 - .../fixtures/p3/wit/deps/random/random.wit | 29 - .../fixtures/p3/wit/deps/random/world.wit | 13 - .../p3/wit/deps/sockets/ip-name-lookup.wit | 62 -- .../fixtures/p3/wit/deps/sockets/types.wit | 725 ----------------- .../fixtures/p3/wit/deps/sockets/world.wit | 9 - .../wasi-cli-0.3.0-rc-2026-02-09/package.wit | 256 ++++++ .../package.wit | 161 ++++ .../package.wit | 566 +++++++++++++ .../package.wit} | 272 ++++--- .../package.wit | 92 +++ .../package.wit | 758 ++++++++++++++++++ packages/jco/test/fixtures/p3/wit/world.wit | 4 +- packages/preview3-shim/lib/nodejs/cli.js | 82 +- packages/preview3-shim/lib/nodejs/clocks.js | 52 +- .../lib/nodejs/filesystem/descriptor.js | 20 +- .../preview3-shim/lib/nodejs/http/client.js | 31 +- .../preview3-shim/lib/nodejs/http/request.js | 43 +- .../preview3-shim/lib/nodejs/http/response.js | 41 +- .../preview3-shim/lib/nodejs/http/server.js | 10 +- .../lib/nodejs/sockets/address.js | 16 +- .../preview3-shim/lib/nodejs/sockets/tcp.js | 11 +- .../preview3-shim/lib/nodejs/sockets/udp.js | 10 +- packages/preview3-shim/lib/nodejs/stream.js | 6 +- .../lib/nodejs/workers/filesystem-worker.js | 3 +- .../lib/nodejs/workers/http-worker.js | 2 +- packages/preview3-shim/test/cli.test.js | 46 +- .../preview3-shim/test/filesystem.test.js | 10 +- packages/preview3-shim/test/future.test.js | 43 + .../preview3-shim/test/http/client.test.js | 37 +- .../preview3-shim/test/http/request.test.js | 32 +- .../preview3-shim/test/http/response.test.js | 41 +- .../preview3-shim/test/http/server.test.js | 2 +- packages/preview3-shim/test/tcp.test.js | 2 +- packages/preview3-shim/types/cli.d.ts | 1 + packages/preview3-shim/types/clocks.d.ts | 2 +- packages/preview3-shim/types/http.d.ts | 1 + .../interfaces/wasi-cli-environment.d.ts | 4 +- .../types/interfaces/wasi-cli-exit.d.ts | 2 +- .../types/interfaces/wasi-cli-run.d.ts | 4 +- .../types/interfaces/wasi-cli-stderr.d.ts | 16 +- .../types/interfaces/wasi-cli-stdin.d.ts | 21 +- .../types/interfaces/wasi-cli-stdout.d.ts | 16 +- .../interfaces/wasi-cli-terminal-input.d.ts | 2 +- .../interfaces/wasi-cli-terminal-output.d.ts | 2 +- .../interfaces/wasi-cli-terminal-stderr.d.ts | 2 +- .../interfaces/wasi-cli-terminal-stdin.d.ts | 2 +- .../interfaces/wasi-cli-terminal-stdout.d.ts | 2 +- .../types/interfaces/wasi-cli-types.d.ts | 15 + .../wasi-clocks-monotonic-clock.d.ts | 26 +- .../interfaces/wasi-clocks-system-clock.d.ts | 38 + .../types/interfaces/wasi-clocks-types.d.ts | 5 + .../interfaces/wasi-clocks-wall-clock.d.ts | 30 - .../interfaces/wasi-filesystem-preopens.d.ts | 2 +- .../interfaces/wasi-filesystem-types.d.ts | 29 +- .../types/interfaces/wasi-http-client.d.ts | 9 + .../types/interfaces/wasi-http-handler.d.ts | 10 +- .../types/interfaces/wasi-http-types.d.ts | 78 +- .../interfaces/wasi-random-insecure-seed.d.ts | 4 +- .../interfaces/wasi-random-insecure.d.ts | 2 +- .../types/interfaces/wasi-random-random.d.ts | 2 +- .../wasi-sockets-ip-name-lookup.d.ts | 2 +- .../types/interfaces/wasi-sockets-types.d.ts | 64 +- .../preview3-shim/types/wasi-cli-command.d.ts | 44 +- .../preview3-shim/types/wasi-http-proxy.d.ts | 10 - .../types/wasi-http-service.d.ts | 14 + scripts/update-wasi-p3.sh | 34 + 85 files changed, 2681 insertions(+), 2390 deletions(-) delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/cli/command.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/cli/environment.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/cli/exit.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/cli/imports.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/cli/run.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/cli/stdio.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/cli/terminal.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/clocks/monotonic-clock.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/clocks/timezone.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/clocks/wall-clock.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/clocks/world.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/filesystem/preopens.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/filesystem/types.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/filesystem/world.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/http/handler.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/http/proxy.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/random/insecure-seed.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/random/insecure.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/random/random.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/random/world.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/sockets/ip-name-lookup.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/sockets/types.wit delete mode 100644 packages/jco/test/fixtures/p3/wit/deps/sockets/world.wit create mode 100644 packages/jco/test/fixtures/p3/wit/deps/wasi-cli-0.3.0-rc-2026-02-09/package.wit create mode 100644 packages/jco/test/fixtures/p3/wit/deps/wasi-clocks-0.3.0-rc-2026-02-09/package.wit create mode 100644 packages/jco/test/fixtures/p3/wit/deps/wasi-filesystem-0.3.0-rc-2026-02-09/package.wit rename packages/jco/test/fixtures/p3/wit/deps/{http/types.wit => wasi-http-0.3.0-rc-2026-02-09/package.wit} (68%) create mode 100644 packages/jco/test/fixtures/p3/wit/deps/wasi-random-0.3.0-rc-2026-02-09/package.wit create mode 100644 packages/jco/test/fixtures/p3/wit/deps/wasi-sockets-0.3.0-rc-2026-02-09/package.wit create mode 100644 packages/preview3-shim/types/interfaces/wasi-cli-types.d.ts create mode 100644 packages/preview3-shim/types/interfaces/wasi-clocks-system-clock.d.ts create mode 100644 packages/preview3-shim/types/interfaces/wasi-clocks-types.d.ts delete mode 100644 packages/preview3-shim/types/interfaces/wasi-clocks-wall-clock.d.ts create mode 100644 packages/preview3-shim/types/interfaces/wasi-http-client.d.ts delete mode 100644 packages/preview3-shim/types/wasi-http-proxy.d.ts create mode 100644 packages/preview3-shim/types/wasi-http-service.d.ts create mode 100755 scripts/update-wasi-p3.sh diff --git a/crates/xtask/src/generate/wasi_types.rs b/crates/xtask/src/generate/wasi_types.rs index c634cfe54..610a5d0ae 100644 --- a/crates/xtask/src/generate/wasi_types.rs +++ b/crates/xtask/src/generate/wasi_types.rs @@ -22,7 +22,7 @@ pub(crate) fn run(version: WasiVersion) -> Result<()> { WasiVersion::Preview3 => WasiTypes { wit_path: "./packages/jco/test/fixtures/p3/wit/", target_path: "./packages/preview3-shim/types/", - worlds: &["wasi:http/proxy", "wasi:cli/command"], + worlds: &["wasi:http/service", "wasi:cli/command"], }, }; diff --git a/packages/jco/test/fixtures/p3/wit/deps/cli/command.wit b/packages/jco/test/fixtures/p3/wit/deps/cli/command.wit deleted file mode 100644 index 0310e5151..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/cli/command.wit +++ /dev/null @@ -1,10 +0,0 @@ -package wasi:cli@0.3.0; - -@since(version = 0.3.0) -world command { - @since(version = 0.3.0) - include imports; - - @since(version = 0.3.0) - export run; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/cli/environment.wit b/packages/jco/test/fixtures/p3/wit/deps/cli/environment.wit deleted file mode 100644 index d99dcc0ae..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/cli/environment.wit +++ /dev/null @@ -1,22 +0,0 @@ -@since(version = 0.3.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.3.0) - get-environment: func() -> list>; - - /// Get the POSIX-style arguments to the program. - @since(version = 0.3.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.3.0) - initial-cwd: func() -> option; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/cli/exit.wit b/packages/jco/test/fixtures/p3/wit/deps/cli/exit.wit deleted file mode 100644 index e799a95a2..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/cli/exit.wit +++ /dev/null @@ -1,17 +0,0 @@ -@since(version = 0.3.0) -interface exit { - /// Exit the current instance and any linked instances. - @since(version = 0.3.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/packages/jco/test/fixtures/p3/wit/deps/cli/imports.wit b/packages/jco/test/fixtures/p3/wit/deps/cli/imports.wit deleted file mode 100644 index 5dbc2ede8..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/cli/imports.wit +++ /dev/null @@ -1,34 +0,0 @@ -package wasi:cli@0.3.0; - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - include wasi:clocks/imports@0.3.0; - @since(version = 0.3.0) - include wasi:filesystem/imports@0.3.0; - @since(version = 0.3.0) - include wasi:sockets/imports@0.3.0; - @since(version = 0.3.0) - include wasi:random/imports@0.3.0; - - @since(version = 0.3.0) - import environment; - @since(version = 0.3.0) - import exit; - @since(version = 0.3.0) - import stdin; - @since(version = 0.3.0) - import stdout; - @since(version = 0.3.0) - import stderr; - @since(version = 0.3.0) - import terminal-input; - @since(version = 0.3.0) - import terminal-output; - @since(version = 0.3.0) - import terminal-stdin; - @since(version = 0.3.0) - import terminal-stdout; - @since(version = 0.3.0) - import terminal-stderr; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/cli/run.wit b/packages/jco/test/fixtures/p3/wit/deps/cli/run.wit deleted file mode 100644 index 6dd8b6879..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/cli/run.wit +++ /dev/null @@ -1,6 +0,0 @@ -@since(version = 0.3.0) -interface run { - /// Run the program. - @since(version = 0.3.0) - run: func() -> result; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/cli/stdio.wit b/packages/jco/test/fixtures/p3/wit/deps/cli/stdio.wit deleted file mode 100644 index 6a1208fad..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/cli/stdio.wit +++ /dev/null @@ -1,17 +0,0 @@ -@since(version = 0.3.0) -interface stdin { - @since(version = 0.3.0) - get-stdin: func() -> stream; -} - -@since(version = 0.3.0) -interface stdout { - @since(version = 0.3.0) - set-stdout: func(data: stream); -} - -@since(version = 0.3.0) -interface stderr { - @since(version = 0.3.0) - set-stderr: func(data: stream); -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/cli/terminal.wit b/packages/jco/test/fixtures/p3/wit/deps/cli/terminal.wit deleted file mode 100644 index c37184f4c..000000000 --- a/packages/jco/test/fixtures/p3/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.3.0) -interface terminal-input { - /// The input side of a terminal. - @since(version = 0.3.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.3.0) -interface terminal-output { - /// The output side of a terminal. - @since(version = 0.3.0) - resource terminal-output; -} - -/// An interface providing an optional `terminal-input` for stdin as a -/// link-time authority. -@since(version = 0.3.0) -interface terminal-stdin { - @since(version = 0.3.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.3.0) - get-terminal-stdin: func() -> option; -} - -/// An interface providing an optional `terminal-output` for stdout as a -/// link-time authority. -@since(version = 0.3.0) -interface terminal-stdout { - @since(version = 0.3.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.3.0) - get-terminal-stdout: func() -> option; -} - -/// An interface providing an optional `terminal-output` for stderr as a -/// link-time authority. -@since(version = 0.3.0) -interface terminal-stderr { - @since(version = 0.3.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.3.0) - get-terminal-stderr: func() -> option; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/clocks/monotonic-clock.wit b/packages/jco/test/fixtures/p3/wit/deps/clocks/monotonic-clock.wit deleted file mode 100644 index 809fc446e..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/clocks/monotonic-clock.wit +++ /dev/null @@ -1,45 +0,0 @@ -package wasi:clocks@0.3.0; -/// 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.3.0) -interface monotonic-clock { - /// 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.3.0) - type instant = u64; - - /// A duration of time, in nanoseconds. - @since(version = 0.3.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.3.0) - now: func() -> instant; - - /// Query the resolution of the clock. Returns the duration of time - /// corresponding to a clock tick. - @since(version = 0.3.0) - resolution: func() -> duration; - - /// Wait until the specified instant has occurred. - @since(version = 0.3.0) - wait-until: async func( - when: instant, - ); - - /// Wait for the specified duration has elapsed. - @since(version = 0.3.0) - wait-for: async func( - how-long: duration, - ); -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/clocks/timezone.wit b/packages/jco/test/fixtures/p3/wit/deps/clocks/timezone.wit deleted file mode 100644 index ac9146834..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/clocks/timezone.wit +++ /dev/null @@ -1,55 +0,0 @@ -package wasi:clocks@0.3.0; - -@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/packages/jco/test/fixtures/p3/wit/deps/clocks/wall-clock.wit b/packages/jco/test/fixtures/p3/wit/deps/clocks/wall-clock.wit deleted file mode 100644 index b7a85ab35..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/clocks/wall-clock.wit +++ /dev/null @@ -1,46 +0,0 @@ -package wasi:clocks@0.3.0; -/// 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.3.0) -interface wall-clock { - /// A time and date in seconds plus nanoseconds. - @since(version = 0.3.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.3.0) - now: func() -> datetime; - - /// Query the resolution of the clock. - /// - /// The nanoseconds field of the output is always less than 1000000000. - @since(version = 0.3.0) - resolution: func() -> datetime; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/clocks/world.wit b/packages/jco/test/fixtures/p3/wit/deps/clocks/world.wit deleted file mode 100644 index f97bcfef1..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/clocks/world.wit +++ /dev/null @@ -1,11 +0,0 @@ -package wasi:clocks@0.3.0; - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - import monotonic-clock; - @since(version = 0.3.0) - import wall-clock; - @unstable(feature = clocks-timezone) - import timezone; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/filesystem/preopens.wit b/packages/jco/test/fixtures/p3/wit/deps/filesystem/preopens.wit deleted file mode 100644 index 0b29aae33..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/filesystem/preopens.wit +++ /dev/null @@ -1,11 +0,0 @@ -package wasi:filesystem@0.3.0; - -@since(version = 0.3.0) -interface preopens { - @since(version = 0.3.0) - use types.{descriptor}; - - /// Return the set of preopened directories, and their paths. - @since(version = 0.3.0) - get-directories: func() -> list>; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/filesystem/types.wit b/packages/jco/test/fixtures/p3/wit/deps/filesystem/types.wit deleted file mode 100644 index 03958752c..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/filesystem/types.wit +++ /dev/null @@ -1,629 +0,0 @@ -package wasi:filesystem@0.3.0; -/// 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.3.0) -interface types { - @since(version = 0.3.0) - use wasi:clocks/wall-clock@0.3.0.{datetime}; - - /// File size or length of a region within a file. - @since(version = 0.3.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.3.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.3.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.3.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.3.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.3.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.3.0) - type link-count = u64; - - /// When setting a timestamp, this gives the value to set it to. - @since(version = 0.3.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, - /// 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.3.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.3.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.3.0) - resource descriptor { - /// Return a stream for reading from a file. - /// - /// Multiple read, write, and append streams may be active on the same open - /// file and they do not interfere with each other. - /// - /// This function returns a future, which will resolve to an error code if - /// reading full contents of the file fails. - /// - /// Note: This is similar to `pread` in POSIX. - @since(version = 0.3.0) - read-via-stream: func( - /// The offset within the file at which to start reading. - offset: filesize, - ) -> tuple, future>>; - - /// Return a stream for writing to a file, if available. - /// - /// May fail with an error-code describing why the file cannot be written. - /// - /// 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. - /// - /// This function returns once either full contents of the stream are - /// written or an error is encountered. - /// - /// Note: This is similar to `pwrite` in POSIX. - @since(version = 0.3.0) - write-via-stream: async func( - /// Data to write - data: stream, - /// The offset within the file at which to start writing. - offset: filesize, - ) -> result<_, error-code>; - - /// Return a stream for appending to a file, if available. - /// - /// May fail with an error-code describing why the file cannot be appended. - /// - /// This function returns once either full contents of the stream are - /// written or an error is encountered. - /// - /// Note: This is similar to `write` with `O_APPEND` in POSIX. - @since(version = 0.3.0) - append-via-stream: async func(data: stream) -> result<_, error-code>; - - /// Provide file advisory information on a descriptor. - /// - /// This is similar to `posix_fadvise` in POSIX. - @since(version = 0.3.0) - advise: async 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.3.0) - sync-data: async 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.3.0) - get-flags: async 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.3.0) - get-type: async 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.3.0) - set-size: async 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.3.0) - set-times: async 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 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. - /// - /// This function returns a future, which will resolve to an error code if - /// reading full contents of the directory fails. - @since(version = 0.3.0) - read-directory: async func() -> tuple, future>>; - - /// 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.3.0) - sync: async func() -> result<_, error-code>; - - /// Create a directory. - /// - /// Note: This is similar to `mkdirat` in POSIX. - @since(version = 0.3.0) - create-directory-at: async 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.3.0) - stat: async 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.3.0) - stat-at: async 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.3.0) - set-times-at: async func( - /// Flags determining the method of how the path is resolved. - path-flags: path-flags, - /// The relative path of the file or directory to operate on. - path: string, - /// The desired values of the data access timestamp. - data-access-timestamp: new-timestamp, - /// The desired values of the data modification timestamp. - data-modification-timestamp: new-timestamp, - ) -> result<_, error-code>; - - /// Create a hard link. - /// - /// Fails with `error-code::no-entry` if the old path does not exist, - /// with `error-code::exist` if the new path already exists, and - /// `error-code::not-permitted` if the old path is not a file. - /// - /// Note: This is similar to `linkat` in POSIX. - @since(version = 0.3.0) - link-at: async func( - /// Flags determining the method of how the path is resolved. - old-path-flags: path-flags, - /// The relative source path from which to link. - old-path: string, - /// The base directory for `new-path`. - new-descriptor: borrow, - /// The relative destination path at which to create the hard link. - new-path: string, - ) -> result<_, error-code>; - - /// Open a file or directory. - /// - /// If `flags` contains `descriptor-flags::mutate-directory`, and the base - /// descriptor doesn't have `descriptor-flags::mutate-directory` set, - /// `open-at` fails with `error-code::read-only`. - /// - /// If `flags` contains `write` or `mutate-directory`, or `open-flags` - /// contains `truncate` or `create`, and the base descriptor doesn't have - /// `descriptor-flags::mutate-directory` set, `open-at` fails with - /// `error-code::read-only`. - /// - /// Note: This is similar to `openat` in POSIX. - @since(version = 0.3.0) - open-at: async 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.3.0) - readlink-at: async 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.3.0) - remove-directory-at: async 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.3.0) - rename-at: async 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.3.0) - symlink-at: async 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.3.0) - unlink-file-at: async 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.3.0) - is-same-object: async func(other: borrow) -> bool; - - /// Return a hash of the metadata associated with a filesystem object referred - /// to by a descriptor. - /// - /// This returns a hash of the last-modification timestamp and file size, and - /// may also include the inode number, device number, birth timestamp, and - /// other metadata fields that may change when the file is modified or - /// replaced. It may also include a secret value chosen by the - /// implementation and not otherwise exposed. - /// - /// Implementations are encouraged to provide the following properties: - /// - /// - If the file is not modified or replaced, the computed hash value should - /// usually not change. - /// - If the object is modified or replaced, the computed hash value should - /// usually change. - /// - The inputs to the hash should not be easily computable from the - /// computed hash. - /// - /// However, none of these is required. - @since(version = 0.3.0) - metadata-hash: async 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.3.0) - metadata-hash-at: async 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; - } -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/filesystem/world.wit b/packages/jco/test/fixtures/p3/wit/deps/filesystem/world.wit deleted file mode 100644 index c0ab32afe..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/filesystem/world.wit +++ /dev/null @@ -1,9 +0,0 @@ -package wasi:filesystem@0.3.0; - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - import types; - @since(version = 0.3.0) - import preopens; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/http/handler.wit b/packages/jco/test/fixtures/p3/wit/deps/http/handler.wit deleted file mode 100644 index e4446cbec..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/http/handler.wit +++ /dev/null @@ -1,17 +0,0 @@ -/// This interface defines a handler of HTTP Requests. It may be imported by -/// components which wish to send HTTP Requests and also exported by components -/// which can respond to HTTP Requests. In addition, it may be used to pass -/// a request from one component to another without any use of a network. -interface handler { - use types.{request, response, error-code}; - - /// When exported, this function may be called with either an incoming - /// request read from the network or a request synthesized or forwarded by - /// another component. - /// - /// When imported, this function may be used to either send an outgoing - /// request over the network or pass it to another component. - handle: async func( - request: request, - ) -> result; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/http/proxy.wit b/packages/jco/test/fixtures/p3/wit/deps/http/proxy.wit deleted file mode 100644 index 16688b96e..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/http/proxy.wit +++ /dev/null @@ -1,44 +0,0 @@ -package wasi:http@0.3.0-draft; - -/// The `wasi:http/imports` world imports all the APIs for HTTP proxies. -/// It is intended to be `include`d in other worlds. -world imports { - /// HTTP proxies have access to time and randomness. - include wasi:clocks/imports@0.3.0; - import wasi:random/random@0.3.0; - - /// Proxies have standard output and error streams which are expected to - /// terminate in a developer-facing console provided by the host. - import wasi:cli/stdout@0.3.0; - import wasi:cli/stderr@0.3.0; - - /// 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. - import wasi:cli/stdin@0.3.0; - - /// This is the default handler to use when user code simply wants to make an - /// HTTP request (e.g., via `fetch()`). - /// - /// This may also be used to pass synthesized or forwarded requests to another - /// component. - import 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. -world proxy { - 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`. - /// - /// This may also be used to receive synthesized or forwarded requests from - /// another component. - export handler; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/random/insecure-seed.wit b/packages/jco/test/fixtures/p3/wit/deps/random/insecure-seed.wit deleted file mode 100644 index 4708d9049..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/random/insecure-seed.wit +++ /dev/null @@ -1,27 +0,0 @@ -package wasi:random@0.3.0; -/// 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.3.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.3.0) - insecure-seed: func() -> tuple; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/random/insecure.wit b/packages/jco/test/fixtures/p3/wit/deps/random/insecure.wit deleted file mode 100644 index 4ea5e581f..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/random/insecure.wit +++ /dev/null @@ -1,25 +0,0 @@ -package wasi:random@0.3.0; -/// 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.3.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.3.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.3.0) - get-insecure-random-u64: func() -> u64; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/random/random.wit b/packages/jco/test/fixtures/p3/wit/deps/random/random.wit deleted file mode 100644 index 786ef25f6..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/random/random.wit +++ /dev/null @@ -1,29 +0,0 @@ -package wasi:random@0.3.0; -/// WASI Random is a random data API. -/// -/// It is intended to be portable at least between Unix-family platforms and -/// Windows. -@since(version = 0.3.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.3.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.3.0) - get-random-u64: func() -> u64; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/random/world.wit b/packages/jco/test/fixtures/p3/wit/deps/random/world.wit deleted file mode 100644 index 838d38023..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/random/world.wit +++ /dev/null @@ -1,13 +0,0 @@ -package wasi:random@0.3.0; - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - import random; - - @since(version = 0.3.0) - import insecure; - - @since(version = 0.3.0) - import insecure-seed; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/sockets/ip-name-lookup.wit b/packages/jco/test/fixtures/p3/wit/deps/sockets/ip-name-lookup.wit deleted file mode 100644 index 73b4b201f..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/sockets/ip-name-lookup.wit +++ /dev/null @@ -1,62 +0,0 @@ -@since(version = 0.3.0) -interface ip-name-lookup { - @since(version = 0.3.0) - use types.{ip-address}; - - /// Lookup error codes. - @since(version = 0.3.0) - enum error-code { - /// Unknown error - unknown, - - /// Access denied. - /// - /// POSIX equivalent: EACCES, EPERM - access-denied, - - /// `name` is a syntactically invalid domain name or IP address. - /// - /// POSIX equivalent: EINVAL - invalid-argument, - - /// Name does not exist or has no suitable associated IP addresses. - /// - /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY - name-unresolvable, - - /// A temporary failure in name resolution occurred. - /// - /// POSIX equivalent: EAI_AGAIN - temporary-resolver-failure, - - /// A permanent failure in name resolution occurred. - /// - /// POSIX equivalent: EAI_FAIL - permanent-resolver-failure, - } - - /// 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. - /// - /// The results are returned in connection order preference. - /// - /// This function never succeeds with 0 results. It either fails or succeeds - /// with at least one address. Additionally, this function never returns - /// IPv4-mapped IPv6 addresses. - /// - /// The returned future will resolve to an error code in case of failure. - /// It will resolve to success once the returned stream is exhausted. - /// - /// # References: - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - resolve-addresses: async func(name: string) -> result, error-code>; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/sockets/types.wit b/packages/jco/test/fixtures/p3/wit/deps/sockets/types.wit deleted file mode 100644 index 7cfb73959..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/sockets/types.wit +++ /dev/null @@ -1,725 +0,0 @@ -@since(version = 0.3.0) -interface types { - @since(version = 0.3.0) - use wasi:clocks/monotonic-clock@0.3.0.{duration}; - - /// 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` - /// - /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. - @since(version = 0.3.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, - - /// The operation is not valid in the socket's current state. - invalid-state, - - /// 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, - } - - @since(version = 0.3.0) - enum ip-address-family { - /// Similar to `AF_INET` in POSIX. - ipv4, - - /// Similar to `AF_INET6` in POSIX. - ipv6, - } - - @since(version = 0.3.0) - type ipv4-address = tuple; - @since(version = 0.3.0) - type ipv6-address = tuple; - - @since(version = 0.3.0) - variant ip-address { - ipv4(ipv4-address), - ipv6(ipv6-address), - } - - @since(version = 0.3.0) - record ipv4-socket-address { - /// sin_port - port: u16, - /// sin_addr - address: ipv4-address, - } - - @since(version = 0.3.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.3.0) - variant ip-socket-address { - ipv4(ipv4-socket-address), - ipv6(ipv6-socket-address), - } - - /// A TCP socket resource. - /// - /// The socket can be in one of the following states: - /// - `unbound` - /// - `bound` (See note below) - /// - `listening` - /// - `connecting` - /// - `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`, `listening`, `connecting` or `connected`) - /// - /// In addition to the general error codes documented on the - /// `types::error-code` type, TCP socket methods may always return - /// `error(invalid-state)` when in the `closed` state. - @since(version = 0.3.0) - resource 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. - /// - /// Unlike POSIX, WASI sockets have no notion of a socket-level - /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's - /// async support. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - constructor(address-family: ip-address-family); - - /// Bind the socket to 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 can be bound to. (EADDRNOTAVAIL) - /// - /// # 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. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - bind: func(local-address: ip-socket-address) -> result<_, error-code>; - - /// Connect to a remote endpoint. - /// - /// On success, the socket is transitioned into the `connected` state and this function returns a connection resource. - /// - /// 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-state`: The socket is already in the `connecting` state. (EALREADY) - /// - `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) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - connect: async func(remote-address: ip-socket-address) -> result<_, error-code>; - - /// Start listening return a stream of new inbound connections. - /// - /// Transitions the socket into the `listening` state. This can be called - /// at most once per socket. - /// - /// If the socket is not already explicitly bound, this function will - /// implicitly bind the socket to a random free port. - /// - /// Normally, the returned sockets are bound, in the `connected` state - /// and immediately ready for I/O. Though, depending on exact timing and - /// circumstances, a newly accepted connection may already be `closed` - /// by the time the server attempts to perform its first I/O on it. This - /// is true regardless of whether the WASI implementation uses - /// "synthesized" sockets or not (see Implementors Notes below). - /// - /// 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` - /// - /// # Typical errors - /// - `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) - /// - /// # Implementors note - /// This method returns a single perpetual stream that should only close - /// on fatal errors (if any). Yet, the POSIX' `accept` function may also - /// return transient errors (e.g. ECONNABORTED). The exact details differ - /// per operation system. For example, the Linux manual mentions: - /// - /// > Linux accept() passes already-pending network errors on the new - /// > socket as an error code from accept(). This behavior differs from - /// > other BSD socket implementations. For reliable operation the - /// > application should detect the network errors defined for the - /// > protocol after accept() and treat them like EAGAIN by retrying. - /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, - /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. - /// Source: https://man7.org/linux/man-pages/man2/accept.2.html - /// - /// WASI implementations have two options to handle this: - /// - Optionally log it and then skip over non-fatal errors returned by - /// `accept`. Guest code never gets to see these failures. Or: - /// - Synthesize a `tcp-socket` resource that exposes the error when - /// attempting to send or receive on it. Guest code then sees these - /// failures as regular I/O errors. - /// - /// In either case, the stream returned by this `listen` method remains - /// operational. - /// - /// # References - /// - - /// - - /// - - /// - - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - listen: func() -> result, error-code>; - - /// Transmit data to peer. - /// - /// The caller should close the stream when it has no more data to send - /// to the peer. Under normal circumstances this will cause a FIN packet - /// to be sent out. Closing the stream is equivalent to calling - /// `shutdown(SHUT_WR)` in POSIX. - /// - /// This function may be called at most once and returns once the full - /// contents of the stream are transmitted or an error is encountered. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) - /// - `connection-reset`: The connection was reset. (ECONNRESET) - /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - send: async func(data: stream) -> result<_, error-code>; - - /// Read data from peer. - /// - /// This function returns a `stream` which provides the data received from the - /// socket, and a `future` providing additional error information in case the - /// socket is closed abnormally. - /// - /// If the socket is closed normally, `stream.read` on the `stream` will return - /// `read-status::closed` with no `error-context` and the future resolves to - /// the value `ok`. If the socket is closed abnormally, `stream.read` on the - /// `stream` returns `read-status::closed` with an `error-context` and the future - /// resolves to `err` with an `error-code`. - /// - /// `receive` is meant to be called only once per socket. If it is called more - /// than once, the subsequent calls return a new `stream` that fails as if it - /// were closed abnormally. - /// - /// If the caller is not expecting to receive any data from the peer, - /// they may drop the stream. Any data still in the receive queue - /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` - /// in POSIX. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) - /// - `connection-reset`: The connection was reset. (ECONNRESET) - /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - receive: func() -> tuple, future>>; - - /// 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.3.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.3.0) - remote-address: func() -> result; - - /// Whether the socket is in the `listening` state. - /// - /// Equivalent to the SO_ACCEPTCONN socket option. - @since(version = 0.3.0) - is-listening: func() -> bool; - - /// Whether this is a IPv4 or IPv6 socket. - /// - /// This is the value passed to the constructor. - /// - /// Equivalent to the SO_DOMAIN socket option. - @since(version = 0.3.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 `connecting` or `connected` state. - @since(version = 0.3.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.3.0) - keep-alive-enabled: func() -> result; - @since(version = 0.3.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.3.0) - keep-alive-idle-time: func() -> result; - @since(version = 0.3.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.3.0) - keep-alive-interval: func() -> result; - @since(version = 0.3.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.3.0) - keep-alive-count: func() -> result; - @since(version = 0.3.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.3.0) - hop-limit: func() -> result; - @since(version = 0.3.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.3.0) - receive-buffer-size: func() -> result; - @since(version = 0.3.0) - set-receive-buffer-size: func(value: u64) -> result<_, error-code>; - @since(version = 0.3.0) - send-buffer-size: func() -> result; - @since(version = 0.3.0) - set-send-buffer-size: func(value: u64) -> result<_, error-code>; - } - - /// A UDP socket handle. - @since(version = 0.3.0) - resource 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. - /// - /// Unlike POSIX, WASI sockets have no notion of a socket-level - /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's - /// async support. - /// - /// # References: - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - constructor(address-family: ip-address-family); - - /// Bind the socket to 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 can be bound to. (EADDRNOTAVAIL) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - bind: func(local-address: ip-socket-address) -> result<_, error-code>; - - /// Associate this socket with a specific peer address. - /// - /// 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`. If the socket was not already explicitly - /// bound, this function will implicitly bind the socket to a random - /// free port. - /// - /// When a UDP socket is "connected", the `send` and `receive` methods - /// are limited to communicating with that peer only: - /// - `send` can only be used to send to this destination. - /// - `receive` will only return datagrams sent from the provided `remote-address`. - /// - /// The name "connect" was kept to align with the existing POSIX - /// terminology. Other than that, this function only changes the local - /// socket configuration and does not generate any network traffic. - /// The peer is not aware of this "connection". - /// - /// This method may be called multiple times on the same socket to change - /// its association, but only the most recent one will be effective. - /// - /// # 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) - /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) - /// - /// # Implementors note - /// If the socket is already connected, some platforms (e.g. Linux) - /// require a disconnect before connecting to a different peer address. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - connect: func(remote-address: ip-socket-address) -> result<_, error-code>; - - /// Dissociate this socket from its peer address. - /// - /// After calling this method, `send` & `receive` are free to communicate - /// with any address again. - /// - /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not connected. - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - disconnect: func() -> result<_, error-code>; - - /// Send a message on the socket to a particular peer. - /// - /// If the socket is connected, the peer address may be left empty. In - /// that case this is equivalent to `send` in POSIX. Otherwise it is - /// equivalent to `sendto`. - /// - /// Additionally, if the socket is connected, a `remote-address` argument - /// _may_ be provided but then it must be identical to the address - /// passed to `connect`. - /// - /// Implementations may trap if the `data` length exceeds 64 KiB. - /// - /// # 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 `connect`. (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.3.0) - send: async func(data: list, remote-address: option) -> result<_, error-code>; - - /// Receive a message on the socket. - /// - /// On success, the return value contains a tuple of the received data - /// and the address of the sender. Theoretical maximum length of the - /// data is 64 KiB. Though in practice, it will typically be less than - /// 1500 bytes. - /// - /// If the socket is connected, the sender address is guaranteed to - /// match the remote address passed to `connect`. - /// - /// # Typical errors - /// - `invalid-state`: The socket has not been bound yet. - /// - `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.3.0) - receive: async func() -> result, ip-socket-address>, 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.3.0) - local-address: func() -> result; - - /// Get the address the socket is currently "connected" to. - /// - /// # Typical errors - /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) - /// - /// # References - /// - - /// - - /// - - /// - - @since(version = 0.3.0) - remote-address: func() -> result; - - /// Whether this is a IPv4 or IPv6 socket. - /// - /// This is the value passed to the constructor. - /// - /// Equivalent to the SO_DOMAIN socket option. - @since(version = 0.3.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.3.0) - unicast-hop-limit: func() -> result; - @since(version = 0.3.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.3.0) - receive-buffer-size: func() -> result; - @since(version = 0.3.0) - set-receive-buffer-size: func(value: u64) -> result<_, error-code>; - @since(version = 0.3.0) - send-buffer-size: func() -> result; - @since(version = 0.3.0) - set-send-buffer-size: func(value: u64) -> result<_, error-code>; - } -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/sockets/world.wit b/packages/jco/test/fixtures/p3/wit/deps/sockets/world.wit deleted file mode 100644 index 6c9951d1c..000000000 --- a/packages/jco/test/fixtures/p3/wit/deps/sockets/world.wit +++ /dev/null @@ -1,9 +0,0 @@ -package wasi:sockets@0.3.0; - -@since(version = 0.3.0) -world imports { - @since(version = 0.3.0) - import types; - @since(version = 0.3.0) - import ip-name-lookup; -} diff --git a/packages/jco/test/fixtures/p3/wit/deps/wasi-cli-0.3.0-rc-2026-02-09/package.wit b/packages/jco/test/fixtures/p3/wit/deps/wasi-cli-0.3.0-rc-2026-02-09/package.wit new file mode 100644 index 000000000..4f8bbf8a2 --- /dev/null +++ b/packages/jco/test/fixtures/p3/wit/deps/wasi-cli-0.3.0-rc-2026-02-09/package.wit @@ -0,0 +1,256 @@ +package wasi:cli@0.3.0-rc-2026-02-09; + +@since(version = 0.3.0-rc-2026-02-09) +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.3.0-rc-2026-02-09) + get-environment: func() -> list>; + + /// Get the POSIX-style arguments to the program. + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-initial-cwd: func() -> option; +} + +@since(version = 0.3.0-rc-2026-02-09) +interface exit { + /// Exit the current instance and any linked instances. + @since(version = 0.3.0-rc-2026-02-09) + 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); +} + +@since(version = 0.3.0-rc-2026-02-09) +interface run { + /// Run the program. + @since(version = 0.3.0-rc-2026-02-09) + run: async func() -> result; +} + +@since(version = 0.3.0-rc-2026-02-09) +interface types { + @since(version = 0.3.0-rc-2026-02-09) + enum error-code { + /// Input/output error + io, + /// Invalid or incomplete multibyte or wide character + illegal-byte-sequence, + /// Broken pipe + pipe, + } +} + +@since(version = 0.3.0-rc-2026-02-09) +interface stdin { + use types.{error-code}; + + /// Return a stream for reading from stdin. + /// + /// This function returns a stream which provides data read from stdin, + /// and a future to signal read results. + /// + /// If the stream's readable end is dropped the future will resolve to success. + /// + /// If the stream's writable end is dropped the future will either resolve to + /// success if stdin was closed by the writer or to an error-code if reading + /// failed for some other reason. + /// + /// Multiple streams may be active at the same time. The behavior of concurrent + /// reads is implementation-specific. + @since(version = 0.3.0-rc-2026-02-09) + read-via-stream: func() -> tuple, future>>; +} + +@since(version = 0.3.0-rc-2026-02-09) +interface stdout { + use types.{error-code}; + + /// Write the given stream to stdout. + /// + /// If the stream's writable end is dropped this function will either return + /// success once the entire contents of the stream have been written or an + /// error-code representing a failure. + /// + /// Otherwise if there is an error the readable end of the stream will be + /// dropped and this function will return an error-code. + @since(version = 0.3.0-rc-2026-02-09) + write-via-stream: func(data: stream) -> future>; +} + +@since(version = 0.3.0-rc-2026-02-09) +interface stderr { + use types.{error-code}; + + /// Write the given stream to stderr. + /// + /// If the stream's writable end is dropped this function will either return + /// success once the entire contents of the stream have been written or an + /// error-code representing a failure. + /// + /// Otherwise if there is an error the readable end of the stream will be + /// dropped and this function will return an error-code. + @since(version = 0.3.0-rc-2026-02-09) + write-via-stream: func(data: stream) -> future>; +} + +/// 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.3.0-rc-2026-02-09) +interface terminal-input { + /// The input side of a terminal. + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) +interface terminal-output { + /// The output side of a terminal. + @since(version = 0.3.0-rc-2026-02-09) + resource terminal-output; +} + +/// An interface providing an optional `terminal-input` for stdin as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-02-09) +interface terminal-stdin { + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-terminal-stdin: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stdout as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-02-09) +interface terminal-stdout { + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-terminal-stdout: func() -> option; +} + +/// An interface providing an optional `terminal-output` for stderr as a +/// link-time authority. +@since(version = 0.3.0-rc-2026-02-09) +interface terminal-stderr { + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-terminal-stderr: func() -> option; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import environment; + @since(version = 0.3.0-rc-2026-02-09) + import exit; + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import stdin; + @since(version = 0.3.0-rc-2026-02-09) + import stdout; + @since(version = 0.3.0-rc-2026-02-09) + import stderr; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-input; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-output; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stdin; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stdout; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stderr; + import wasi:clocks/types@0.3.0-rc-2026-02-09; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09; + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-02-09; + import wasi:filesystem/types@0.3.0-rc-2026-02-09; + import wasi:filesystem/preopens@0.3.0-rc-2026-02-09; + import wasi:sockets/types@0.3.0-rc-2026-02-09; + import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-02-09; + import wasi:random/random@0.3.0-rc-2026-02-09; + import wasi:random/insecure@0.3.0-rc-2026-02-09; + import wasi:random/insecure-seed@0.3.0-rc-2026-02-09; +} +@since(version = 0.3.0-rc-2026-02-09) +world command { + @since(version = 0.3.0-rc-2026-02-09) + import environment; + @since(version = 0.3.0-rc-2026-02-09) + import exit; + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import stdin; + @since(version = 0.3.0-rc-2026-02-09) + import stdout; + @since(version = 0.3.0-rc-2026-02-09) + import stderr; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-input; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-output; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stdin; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stdout; + @since(version = 0.3.0-rc-2026-02-09) + import terminal-stderr; + import wasi:clocks/types@0.3.0-rc-2026-02-09; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09; + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-02-09; + import wasi:filesystem/types@0.3.0-rc-2026-02-09; + import wasi:filesystem/preopens@0.3.0-rc-2026-02-09; + import wasi:sockets/types@0.3.0-rc-2026-02-09; + import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-02-09; + import wasi:random/random@0.3.0-rc-2026-02-09; + import wasi:random/insecure@0.3.0-rc-2026-02-09; + import wasi:random/insecure-seed@0.3.0-rc-2026-02-09; + + @since(version = 0.3.0-rc-2026-02-09) + export run; +} diff --git a/packages/jco/test/fixtures/p3/wit/deps/wasi-clocks-0.3.0-rc-2026-02-09/package.wit b/packages/jco/test/fixtures/p3/wit/deps/wasi-clocks-0.3.0-rc-2026-02-09/package.wit new file mode 100644 index 000000000..71986bab2 --- /dev/null +++ b/packages/jco/test/fixtures/p3/wit/deps/wasi-clocks-0.3.0-rc-2026-02-09/package.wit @@ -0,0 +1,161 @@ +package wasi:clocks@0.3.0-rc-2026-02-09; + +/// This interface common types used throughout wasi:clocks. +@since(version = 0.3.0-rc-2026-02-09) +interface types { + /// A duration of time, in nanoseconds. + @since(version = 0.3.0-rc-2026-02-09) + type duration = u64; +} + +/// 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.3.0-rc-2026-02-09) +interface monotonic-clock { + use types.{duration}; + + /// A mark on a monotonic clock is a number of nanoseconds since an + /// unspecified initial value, and can only be compared to instances from + /// the same monotonic-clock. + @since(version = 0.3.0-rc-2026-02-09) + type mark = 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. + /// + /// For completeness, this function traps if it's not possible to represent + /// the value of the clock in a `mark`. Consequently, implementations + /// should ensure that the starting time is low enough to avoid the + /// possibility of overflow in practice. + @since(version = 0.3.0-rc-2026-02-09) + now: func() -> mark; + + /// Query the resolution of the clock. Returns the duration of time + /// corresponding to a clock tick. + @since(version = 0.3.0-rc-2026-02-09) + get-resolution: func() -> duration; + + /// Wait until the specified mark has occurred. + @since(version = 0.3.0-rc-2026-02-09) + wait-until: async func(when: mark); + + /// Wait for the specified duration to elapse. + @since(version = 0.3.0-rc-2026-02-09) + wait-for: async func(how-long: duration); +} + +/// WASI System Clock is a clock API intended to let users query the current +/// time. The clock is not necessarily monotonic as it may be reset. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +/// +/// 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.3.0-rc-2026-02-09) +interface system-clock { + use types.{duration}; + + /// An "instant", or "exact time", is a point in time without regard to any + /// time zone: just the time since a particular external reference point, + /// often called an "epoch". + /// + /// Here, the epoch is 1970-01-01T00:00:00Z, also known as + /// [POSIX's Seconds Since the Epoch], also known as [Unix Time]. + /// + /// Note that even if the seconds field is negative, incrementing + /// nanoseconds always represents moving forwards in time. + /// For example, `{ -1 seconds, 999999999 nanoseconds }` represents the + /// instant one nanosecond before the epoch. + /// For more on various different ways to represent time, see + /// https://tc39.es/proposal-temporal/docs/timezone.html + /// + /// [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.3.0-rc-2026-02-09) + record instant { + seconds: s64, + 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 nanoseconds field of the output is always less than 1000000000. + @since(version = 0.3.0-rc-2026-02-09) + now: func() -> instant; + + /// Query the resolution of the clock. Returns the smallest duration of time + /// that the implementation permits distinguishing. + @since(version = 0.3.0-rc-2026-02-09) + get-resolution: func() -> duration; +} + +@unstable(feature = clocks-timezone) +interface timezone { + @unstable(feature = clocks-timezone) + use system-clock.{instant}; + + /// Return the IANA identifier of the currently configured timezone. This + /// should be an identifier from the IANA Time Zone Database. + /// + /// For displaying to a user, the identifier should be converted into a + /// localized name by means of an internationalization API. + /// + /// If the implementation does not expose an actual timezone, or is unable + /// to provide mappings from times to deltas between the configured timezone + /// and UTC, or determining the current timezone fails, or the timezone does + /// not have an IANA identifier, this returns nothing. + @unstable(feature = clocks-timezone) + iana-id: func() -> option; + + /// The number of nanoseconds difference between UTC time and the local + /// time of the currently configured timezone, at the exact time of + /// `instant`. + /// + /// The magnitude of the returned value will always be less than + /// 86,400,000,000,000 which is the number of nanoseconds in a day + /// (24*60*60*1e9). + /// + /// If the implementation does not expose an actual timezone, or is unable + /// to provide mappings from times to deltas between the configured timezone + /// and UTC, or determining the current timezone fails, this returns + /// nothing. + @unstable(feature = clocks-timezone) + utc-offset: func(when: instant) -> option; + + /// Returns a string that is suitable to assist humans in debugging whether + /// any timezone is available, and if so, which. This may be the same string + /// as `iana-id`, or a formatted representation of the UTC offset such as + /// `-04:00`, or something else. + /// + /// 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. + @unstable(feature = clocks-timezone) + to-debug-string: func() -> string; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import monotonic-clock; + @since(version = 0.3.0-rc-2026-02-09) + import system-clock; + @unstable(feature = clocks-timezone) + import timezone; +} diff --git a/packages/jco/test/fixtures/p3/wit/deps/wasi-filesystem-0.3.0-rc-2026-02-09/package.wit b/packages/jco/test/fixtures/p3/wit/deps/wasi-filesystem-0.3.0-rc-2026-02-09/package.wit new file mode 100644 index 000000000..8fb83041a --- /dev/null +++ b/packages/jco/test/fixtures/p3/wit/deps/wasi-filesystem-0.3.0-rc-2026-02-09/package.wit @@ -0,0 +1,566 @@ +package wasi:filesystem@0.3.0-rc-2026-02-09; + +/// 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. +/// +/// 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]. +/// +/// Though this package presents a portable interface modelled on POSIX, it +/// prioritizes compatibility over portability: allowing users to access their +/// files on their machine is more important than exposing a single semantics +/// across all platforms. Notably, depending on the underlying operating system +/// and file system: +/// * Paths may be case-folded or not. +/// * Deleting (unlinking) a file may fail if there are other file descriptors +/// open. +/// * Durability and atomicity of changes to underlying files when there are +/// concurrent writers. +/// +/// Users that need well-defined, portable semantics should use a key-value +/// store or a database instead. +/// +/// [WASI filesystem path resolution]: https://github.com/WebAssembly/wasi-filesystem/blob/main/path-resolution.md +@since(version = 0.3.0-rc-2026-02-09) +interface types { + @since(version = 0.3.0-rc-2026-02-09) + use wasi:clocks/system-clock@0.3.0-rc-2026-02-09.{instant}; + + /// File size or length of a region within a file. + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + 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, + } + + /// Flags determining the method of how paths are resolved. + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + type link-count = u64; + + /// File attributes. + /// + /// Note: This was called `filestat` in earlier versions of WASI. + @since(version = 0.3.0-rc-2026-02-09) + 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, + } + + /// When setting a timestamp, this gives the value to set it to. + @since(version = 0.3.0-rc-2026-02-09) + 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(instant), + } + + /// A directory entry. + @since(version = 0.3.0-rc-2026-02-09) + 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. + @since(version = 0.3.0-rc-2026-02-09) + enum error-code { + /// Permission denied, similar to `EACCES` in POSIX. + access, + /// 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.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + resource descriptor { + /// Return a stream for reading from a file. + /// + /// Multiple read, write, and append streams may be active on the same open + /// file and they do not interfere with each other. + /// + /// This function returns a `stream` which provides the data received from the + /// file, and a `future` providing additional error information in case an + /// error is encountered. + /// + /// If no error is encountered, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If an error is encountered, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// Note: This is similar to `pread` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + read-via-stream: func(offset: filesize) -> tuple, future>>; + /// Return a stream for writing to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be written. + /// + /// 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. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `pwrite` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + write-via-stream: func(data: stream, offset: filesize) -> future>; + /// Return a stream for appending to a file, if available. + /// + /// May fail with an error-code describing why the file cannot be appended. + /// + /// This function returns once either full contents of the stream are + /// written or an error is encountered. + /// + /// Note: This is similar to `write` with `O_APPEND` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + append-via-stream: func(data: stream) -> future>; + /// Provide file advisory information on a descriptor. + /// + /// This is similar to `posix_fadvise` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + advise: async func(offset: filesize, length: filesize, 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.3.0-rc-2026-02-09) + sync-data: async 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.3.0-rc-2026-02-09) + get-flags: async 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.3.0-rc-2026-02-09) + get-type: async 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.3.0-rc-2026-02-09) + set-size: async 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.3.0-rc-2026-02-09) + set-times: async func(data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + /// 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. + /// + /// This function returns a future, which will resolve to an error code if + /// reading full contents of the directory fails. + @since(version = 0.3.0-rc-2026-02-09) + read-directory: func() -> tuple, future>>; + /// 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.3.0-rc-2026-02-09) + sync: async func() -> result<_, error-code>; + /// Create a directory. + /// + /// Note: This is similar to `mkdirat` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + create-directory-at: async func(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.3.0-rc-2026-02-09) + stat: async 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.3.0-rc-2026-02-09) + stat-at: async func(path-flags: path-flags, 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.3.0-rc-2026-02-09) + set-times-at: async func(path-flags: path-flags, path: string, data-access-timestamp: new-timestamp, data-modification-timestamp: new-timestamp) -> result<_, error-code>; + /// Create a hard link. + /// + /// Fails with `error-code::no-entry` if the old path does not exist, + /// with `error-code::exist` if the new path already exists, and + /// `error-code::not-permitted` if the old path is not a file. + /// + /// Note: This is similar to `linkat` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + link-at: async func(old-path-flags: path-flags, old-path: string, new-descriptor: borrow, new-path: string) -> result<_, error-code>; + /// Open a file or directory. + /// + /// If `flags` contains `descriptor-flags::mutate-directory`, and the base + /// descriptor doesn't have `descriptor-flags::mutate-directory` set, + /// `open-at` fails with `error-code::read-only`. + /// + /// If `flags` contains `write` or `mutate-directory`, or `open-flags` + /// contains `truncate` or `create`, and the base descriptor doesn't have + /// `descriptor-flags::mutate-directory` set, `open-at` fails with + /// `error-code::read-only`. + /// + /// Note: This is similar to `openat` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + open-at: async func(path-flags: path-flags, path: string, open-flags: open-flags, %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.3.0-rc-2026-02-09) + readlink-at: async func(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.3.0-rc-2026-02-09) + remove-directory-at: async func(path: string) -> result<_, error-code>; + /// Rename a filesystem object. + /// + /// Note: This is similar to `renameat` in POSIX. + @since(version = 0.3.0-rc-2026-02-09) + rename-at: async func(old-path: string, new-descriptor: borrow, 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.3.0-rc-2026-02-09) + symlink-at: async func(old-path: string, 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.3.0-rc-2026-02-09) + unlink-file-at: async func(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.3.0-rc-2026-02-09) + is-same-object: async func(other: borrow) -> bool; + /// Return a hash of the metadata associated with a filesystem object referred + /// to by a descriptor. + /// + /// This returns a hash of the last-modification timestamp and file size, and + /// may also include the inode number, device number, birth timestamp, and + /// other metadata fields that may change when the file is modified or + /// replaced. It may also include a secret value chosen by the + /// implementation and not otherwise exposed. + /// + /// Implementations are encouraged to provide the following properties: + /// + /// - If the file is not modified or replaced, the computed hash value should + /// usually not change. + /// - If the object is modified or replaced, the computed hash value should + /// usually change. + /// - The inputs to the hash should not be easily computable from the + /// computed hash. + /// + /// However, none of these is required. + @since(version = 0.3.0-rc-2026-02-09) + metadata-hash: async 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.3.0-rc-2026-02-09) + metadata-hash-at: async func(path-flags: path-flags, path: string) -> result; + } +} + +@since(version = 0.3.0-rc-2026-02-09) +interface preopens { + @since(version = 0.3.0-rc-2026-02-09) + use types.{descriptor}; + + /// Return the set of preopened directories, and their paths. + @since(version = 0.3.0-rc-2026-02-09) + get-directories: func() -> list>; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import wasi:clocks/types@0.3.0-rc-2026-02-09; + @since(version = 0.3.0-rc-2026-02-09) + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import preopens; +} diff --git a/packages/jco/test/fixtures/p3/wit/deps/http/types.wit b/packages/jco/test/fixtures/p3/wit/deps/wasi-http-0.3.0-rc-2026-02-09/package.wit similarity index 68% rename from packages/jco/test/fixtures/p3/wit/deps/http/types.wit rename to packages/jco/test/fixtures/p3/wit/deps/wasi-http-0.3.0-rc-2026-02-09/package.wit index a804ee9fc..442b18ea1 100644 --- a/packages/jco/test/fixtures/p3/wit/deps/http/types.wit +++ b/packages/jco/test/fixtures/p3/wit/deps/wasi-http-0.3.0-rc-2026-02-09/package.wit @@ -1,9 +1,13 @@ +package wasi:http@0.3.0-rc-2026-02-09; + /// This interface defines all of the types and methods for implementing HTTP /// Requests and Responses, as well as their headers, trailers, and bodies. +@since(version = 0.3.0-rc-2026-02-09) interface types { - use wasi:clocks/monotonic-clock@0.3.0.{duration}; + use wasi:clocks/types@0.3.0-rc-2026-02-09.{duration}; /// This type corresponds to HTTP standard Methods. + @since(version = 0.3.0-rc-2026-02-09) variant method { get, head, @@ -14,18 +18,41 @@ interface types { options, trace, patch, - other(string) + other(string), } /// This type corresponds to HTTP standard Related Schemes. + @since(version = 0.3.0-rc-2026-02-09) variant scheme { HTTP, HTTPS, - other(string) + other(string), + } + + /// Defines the case payload type for `DNS-error` above: + @since(version = 0.3.0-rc-2026-02-09) + record DNS-error-payload { + rcode: option, + info-code: option, + } + + /// Defines the case payload type for `TLS-alert-received` above: + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + record field-size-payload { + field-name: option, + field-size: option, } /// These cases are inspired by the IANA HTTP Proxy Error Types: /// + @since(version = 0.3.0-rc-2026-02-09) variant error-code { DNS-timeout, DNS-error(DNS-error-payload), @@ -70,39 +97,20 @@ interface types { /// 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: - record DNS-error-payload { - rcode: option, - info-code: option - } - - /// Defines the case payload type for `TLS-alert-received` above: - record TLS-alert-received-payload { - alert-id: option, - alert-message: option - } - - /// Defines the case payload type for `HTTP-response-{header,trailer}-size` above: - record field-size-payload { - field-name: option, - field-size: option + internal-error(option), } /// This type enumerates the different kinds of errors that may occur when /// setting or appending to a `fields` resource. + @since(version = 0.3.0-rc-2026-02-09) variant header-error { /// This error indicates that a `field-name` or `field-value` was /// syntactically invalid when used with an operation that sets headers in a /// `fields`. invalid-syntax, - /// This error indicates that a forbidden `field-name` was used when trying /// to set a header in a `fields`. forbidden, - /// This error indicates that the operation on the `fields` was not /// permitted because the fields are immutable. immutable, @@ -110,10 +118,10 @@ interface types { /// This type enumerates the different kinds of errors that may occur when /// setting fields of a `request-options` resource. + @since(version = 0.3.0-rc-2026-02-09) variant request-options-error { /// Indicates the specified field is not supported by this implementation. not-supported, - /// Indicates that the operation on the `request-options` was not permitted /// because it is immutable. immutable, @@ -123,11 +131,13 @@ interface types { /// /// Field names should always be treated as case insensitive by the `fields` /// resource for the purposes of equality checking. + @since(version = 0.3.0-rc-2026-02-09) type field-name = 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.3.0-rc-2026-02-09) type field-value = list; /// This following block defines the `fields` resource which corresponds to @@ -145,13 +155,12 @@ interface types { /// original casing used to construct or mutate the `fields` resource. The `fields` /// resource should use that original casing when serializing the fields for /// transport or when returning them from a method. + @since(version = 0.3.0-rc-2026-02-09) resource fields { - /// Construct an empty HTTP Fields. /// /// The resulting `fields` is mutable. constructor(); - /// Construct an HTTP Fields. /// /// The resulting `fields` is mutable. @@ -167,32 +176,25 @@ interface types { /// /// An error result will be returned if any header or value was /// syntactically invalid, or if a header was forbidden. - from-list: static func( - entries: list> - ) -> result; - + from-list: static func(entries: list>) -> result; /// Get all of the values corresponding to a name. If the name is not present /// in this `fields`, an empty list is returned. However, if the name is /// present but empty, this is represented by a list with one or more /// empty field-values present. get: func(name: field-name) -> list; - /// Returns `true` when the name is present in this `fields`. If the name is /// syntactically invalid, `false` is returned. has: func(name: field-name) -> bool; - /// Set all of the values for a name. Clears any existing values for that /// name, if they have been set. /// /// Fails with `header-error.immutable` if the `fields` are immutable. set: func(name: field-name, value: list) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name /// exist. /// /// Fails with `header-error.immutable` if the `fields` are immutable. delete: func(name: field-name) -> result<_, header-error>; - /// Delete all values for a name. Does nothing if no values for the name /// exist. /// @@ -200,13 +202,11 @@ interface types { /// /// Fails with `header-error.immutable` if the `fields` are immutable. get-and-delete: func(name: field-name) -> result, header-error>; - /// Append a value for a name. Does not change or delete any existing /// values for that name. /// /// Fails with `header-error.immutable` if the `fields` are immutable. append: func(name: field-name, value: field-value) -> result<_, header-error>; - /// Retrieve the full set of names and values in the Fields. Like the /// constructor, the list represents each name-value pair. /// @@ -216,29 +216,31 @@ interface types { /// /// The names and values are always returned in the original casing and in /// the order in which they will be serialized for transport. - entries: func() -> list>; - + copy-all: 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` constructor on the return value of `copy-all`. The resulting /// `fields` is mutable. clone: func() -> fields; } /// Headers is an alias for Fields. + @since(version = 0.3.0-rc-2026-02-09) type headers = fields; /// Trailers is an alias for Fields. + @since(version = 0.3.0-rc-2026-02-09) type trailers = fields; /// Represents an HTTP Request. + @since(version = 0.3.0-rc-2026-02-09) resource request { - /// Construct a new `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. /// - /// `contents` is the optional body content stream. + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. /// Once it is closed, `trailers` future must resolve to a result. /// If `trailers` resolves to an error, underlying connection /// will be closed immediately. @@ -253,45 +255,35 @@ interface types { /// to reject invalid constructions of `request`. /// /// The returned future resolves to result of transmission of this request. - new: static func( - headers: headers, - contents: option>, - trailers: future, error-code>>, - options: option - ) -> tuple>>; - + new: static func(headers: headers, contents: option>, trailers: future, error-code>>, options: option) -> tuple>>; /// Get the Method for the Request. - method: func() -> method; + get-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. 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. - path-with-query: func() -> option; + get-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. 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. - scheme: func() -> option; + get-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. 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. - authority: func() -> option; + get-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. set-authority: func(authority: option) -> result; - /// Get the `request-options` to be associated with this request /// /// The returned `request-options` resource is immutable: `set-*` operations @@ -300,34 +292,25 @@ interface types { /// This `request-options` resource is a child: it must be dropped before /// the parent `request` is dropped, or its ownership is transferred to /// another component by e.g. `handler.handle`. - options: func() -> option; - + get-options: 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`. - headers: func() -> headers; - + get-headers: func() -> headers; /// Get body of the Request. /// /// Stream returned by this method represents the contents of the body. - /// Once the stream is reported as closed, callers should await the returned future - /// to determine whether the body was received successfully. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. /// The future will only resolve after the stream is reported as closed. /// - /// The stream and future returned by this method are children: - /// they should be closed or consumed before the parent `response` - /// is dropped, or its ownership is transferred to another component - /// by e.g. `handler.handle`. - /// - /// This method may be called multiple times. + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the request. /// - /// This method will return an error if it is called while either: - /// - a stream or future returned by a previous call to this method is still open - /// - a stream returned by a previous call to this method has reported itself as closed - /// Thus there will always be at most one readable stream open for a given body. - /// Each subsequent stream picks up where the last stream left off, up until it is finished. - body: func() -> result, future, error-code>>>>; + /// Note that function will move the `request`, but references to headers or + /// request options acquired from it previously will remain valid. + consume-body: static func(this: request, res: future>) -> tuple, future, error-code>>>; } /// Parameters for making an HTTP Request. Each of these parameters is @@ -336,96 +319,161 @@ interface types { /// /// These timeouts are separate from any the user may use to bound an /// asynchronous call. + @since(version = 0.3.0-rc-2026-02-09) resource request-options { /// Construct a default `request-options` value. constructor(); - /// The timeout for the initial connect to the HTTP Server. - connect-timeout: func() -> option; - + get-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 or that this /// handle is immutable. set-connect-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving the first byte of the Response body. - first-byte-timeout: func() -> option; - + get-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 or that /// this handle is immutable. set-first-byte-timeout: func(duration: option) -> result<_, request-options-error>; - /// The timeout for receiving subsequent chunks of bytes in the Response /// body stream. - between-bytes-timeout: func() -> option; - + get-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 or that this handle is immutable. set-between-bytes-timeout: func(duration: option) -> result<_, request-options-error>; - /// Make a deep copy of the `request-options`. /// The resulting `request-options` is mutable. clone: func() -> request-options; } /// This type corresponds to the HTTP standard Status Code. + @since(version = 0.3.0-rc-2026-02-09) type status-code = u16; /// Represents an HTTP Response. + @since(version = 0.3.0-rc-2026-02-09) resource response { - /// Construct a new `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. /// - /// `contents` is the optional body content stream. + /// `contents` is the optional body content stream with `none` + /// representing a zero-length content stream. /// Once it is closed, `trailers` future must resolve to a result. /// If `trailers` resolves to an error, underlying connection /// will be closed immediately. /// /// The returned future resolves to result of transmission of this response. - new: static func( - headers: headers, - contents: option>, - trailers: future, error-code>>, - ) -> tuple>>; - + new: static func(headers: headers, contents: option>, trailers: future, error-code>>) -> tuple>>; /// Get the HTTP Status Code for the Response. - status-code: func() -> status-code; - + get-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. set-status-code: func(status-code: status-code) -> result; - /// Get the headers associated with the Response. /// /// The returned `headers` resource is immutable: `set`, `append`, and /// `delete` operations will fail with `header-error.immutable`. - headers: func() -> headers; - + get-headers: func() -> headers; /// Get body of the Response. /// /// Stream returned by this method represents the contents of the body. - /// Once the stream is reported as closed, callers should await the returned future - /// to determine whether the body was received successfully. + /// Once the stream is reported as closed, callers should await the returned + /// future to determine whether the body was received successfully. /// The future will only resolve after the stream is reported as closed. /// - /// The stream and future returned by this method are children: - /// they should be closed or consumed before the parent `response` - /// is dropped, or its ownership is transferred to another component - /// by e.g. `handler.handle`. - /// - /// This method may be called multiple times. + /// This function takes a `res` future as a parameter, which can be used to + /// communicate an error in handling of the response. /// - /// This method will return an error if it is called while either: - /// - a stream or future returned by a previous call to this method is still open - /// - a stream returned by a previous call to this method has reported itself as closed - /// Thus there will always be at most one readable stream open for a given body. - /// Each subsequent stream picks up where the last stream left off, up until it is finished. - body: func() -> result, future, error-code>>>>; + /// Note that function will move the `response`, but references to headers + /// acquired from it previously will remain valid. + consume-body: static func(this: response, res: future>) -> tuple, future, error-code>>>; } } + +/// This interface defines a handler of HTTP Requests. +/// +/// In a `wasi:http/service` this interface is exported to respond to an +/// incoming HTTP Request with a Response. +/// +/// In `wasi:http/middleware` this interface is both exported and imported as +/// the "downstream" and "upstream" directions of the middleware chain. +@since(version = 0.3.0-rc-2026-02-09) +interface handler { + use types.{request, response, error-code}; + + /// This function may be called with either an incoming request read from the + /// network or a request synthesized or forwarded by another component. + handle: async func(request: request) -> result; +} + +/// This interface defines an HTTP client for sending "outgoing" requests. +/// +/// Most components are expected to import this interface to provide the +/// capability to send HTTP requests to arbitrary destinations on a network. +/// +/// The type signature of `client.send` is the same as `handler.handle`. This +/// duplication is currently necessary because some Component Model tooling +/// (including WIT itself) is unable to represent a component importing two +/// instances of the same interface. A `client.send` import may be linked +/// directly to a `handler.handle` export to bypass the network. +@since(version = 0.3.0-rc-2026-02-09) +interface client { + use types.{request, response, error-code}; + + /// This function may be used to either send an outgoing request over the + /// network or to forward it to another component. + send: async func(request: request) -> result; +} + +/// The `wasi:http/service` world captures a broad category of HTTP services +/// including web applications, API servers, and proxies. It may be `include`d +/// in more specific worlds such as `wasi:http/middleware`. +@since(version = 0.3.0-rc-2026-02-09) +world service { + import wasi:cli/types@0.3.0-rc-2026-02-09; + import wasi:cli/stdout@0.3.0-rc-2026-02-09; + import wasi:cli/stderr@0.3.0-rc-2026-02-09; + import wasi:cli/stdin@0.3.0-rc-2026-02-09; + import wasi:clocks/types@0.3.0-rc-2026-02-09; + import types; + import client; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09; + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-02-09; + import wasi:random/random@0.3.0-rc-2026-02-09; + import wasi:random/insecure@0.3.0-rc-2026-02-09; + import wasi:random/insecure-seed@0.3.0-rc-2026-02-09; + + export handler; +} +/// The `wasi:http/middleware` world captures HTTP services that forward HTTP +/// Requests to another handler. +/// +/// Components may implement this world to allow them to participate in handler +/// "chains" where a `request` flows through handlers on its way to some terminal +/// `service` and corresponding `response` flows in the opposite direction. +@since(version = 0.3.0-rc-2026-02-09) +world middleware { + import wasi:clocks/types@0.3.0-rc-2026-02-09; + import types; + import handler; + import wasi:cli/types@0.3.0-rc-2026-02-09; + import wasi:cli/stdout@0.3.0-rc-2026-02-09; + import wasi:cli/stderr@0.3.0-rc-2026-02-09; + import wasi:cli/stdin@0.3.0-rc-2026-02-09; + import client; + import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09; + import wasi:clocks/system-clock@0.3.0-rc-2026-02-09; + @unstable(feature = clocks-timezone) + import wasi:clocks/timezone@0.3.0-rc-2026-02-09; + import wasi:random/random@0.3.0-rc-2026-02-09; + import wasi:random/insecure@0.3.0-rc-2026-02-09; + import wasi:random/insecure-seed@0.3.0-rc-2026-02-09; + + export handler; +} diff --git a/packages/jco/test/fixtures/p3/wit/deps/wasi-random-0.3.0-rc-2026-02-09/package.wit b/packages/jco/test/fixtures/p3/wit/deps/wasi-random-0.3.0-rc-2026-02-09/package.wit new file mode 100644 index 000000000..521df6e50 --- /dev/null +++ b/packages/jco/test/fixtures/p3/wit/deps/wasi-random-0.3.0-rc-2026-02-09/package.wit @@ -0,0 +1,92 @@ +package wasi:random@0.3.0-rc-2026-02-09; + +/// 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.3.0-rc-2026-02-09) +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.3.0-rc-2026-02-09) + get-insecure-seed: func() -> tuple; +} + +/// 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.3.0-rc-2026-02-09) +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.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-insecure-random-u64: func() -> u64; +} + +/// WASI Random is a random data API. +/// +/// It is intended to be portable at least between Unix-family platforms and +/// Windows. +@since(version = 0.3.0-rc-2026-02-09) +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.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-random-u64: func() -> u64; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import random; + @since(version = 0.3.0-rc-2026-02-09) + import insecure; + @since(version = 0.3.0-rc-2026-02-09) + import insecure-seed; +} diff --git a/packages/jco/test/fixtures/p3/wit/deps/wasi-sockets-0.3.0-rc-2026-02-09/package.wit b/packages/jco/test/fixtures/p3/wit/deps/wasi-sockets-0.3.0-rc-2026-02-09/package.wit new file mode 100644 index 000000000..aa9d4d7f0 --- /dev/null +++ b/packages/jco/test/fixtures/p3/wit/deps/wasi-sockets-0.3.0-rc-2026-02-09/package.wit @@ -0,0 +1,758 @@ +package wasi:sockets@0.3.0-rc-2026-02-09; + +@since(version = 0.3.0-rc-2026-02-09) +interface types { + @since(version = 0.3.0-rc-2026-02-09) + use wasi:clocks/types@0.3.0-rc-2026-02-09.{duration}; + + /// 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` + /// + /// See each individual API for what the POSIX equivalents are. They sometimes differ per API. + @since(version = 0.3.0-rc-2026-02-09) + 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, + /// The operation is not valid in the socket's current state. + invalid-state, + /// 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, + } + + @since(version = 0.3.0-rc-2026-02-09) + enum ip-address-family { + /// Similar to `AF_INET` in POSIX. + ipv4, + /// Similar to `AF_INET6` in POSIX. + ipv6, + } + + @since(version = 0.3.0-rc-2026-02-09) + type ipv4-address = tuple; + + @since(version = 0.3.0-rc-2026-02-09) + type ipv6-address = tuple; + + @since(version = 0.3.0-rc-2026-02-09) + variant ip-address { + ipv4(ipv4-address), + ipv6(ipv6-address), + } + + @since(version = 0.3.0-rc-2026-02-09) + record ipv4-socket-address { + /// sin_port + port: u16, + /// sin_addr + address: ipv4-address, + } + + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + variant ip-socket-address { + ipv4(ipv4-socket-address), + ipv6(ipv6-socket-address), + } + + /// A TCP socket resource. + /// + /// The socket can be in one of the following states: + /// - `unbound` + /// - `bound` (See note below) + /// - `listening` + /// - `connecting` + /// - `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`, `listening`, `connecting` or `connected`) + /// + /// In addition to the general error codes documented on the + /// `types::error-code` type, TCP socket methods may always return + /// `error(invalid-state)` when in the `closed` state. + @since(version = 0.3.0-rc-2026-02-09) + resource 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. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + create: static func(address-family: ip-address-family) -> result; + /// Bind the socket to 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 can be bound to. (EADDRNOTAVAIL) + /// + /// # 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. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + /// Connect to a remote endpoint. + /// + /// On success, the socket is transitioned into the `connected` state and this function returns a connection resource. + /// + /// 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-state`: The socket is already in the `connecting` state. (EALREADY) + /// - `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) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + connect: async func(remote-address: ip-socket-address) -> result<_, error-code>; + /// Start listening and return a stream of new inbound connections. + /// + /// Transitions the socket into the `listening` state. This can be called + /// at most once per socket. + /// + /// If the socket is not already explicitly bound, this function will + /// implicitly bind the socket to a random free port. + /// + /// Normally, the returned sockets are bound, in the `connected` state + /// and immediately ready for I/O. Though, depending on exact timing and + /// circumstances, a newly accepted connection may already be `closed` + /// by the time the server attempts to perform its first I/O on it. This + /// is true regardless of whether the WASI implementation uses + /// "synthesized" sockets or not (see Implementors Notes below). + /// + /// 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` + /// + /// # Typical errors + /// - `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) + /// + /// # Implementors note + /// This method returns a single perpetual stream that should only close + /// on fatal errors (if any). Yet, the POSIX' `accept` function may also + /// return transient errors (e.g. ECONNABORTED). The exact details differ + /// per operation system. For example, the Linux manual mentions: + /// + /// > Linux accept() passes already-pending network errors on the new + /// > socket as an error code from accept(). This behavior differs from + /// > other BSD socket implementations. For reliable operation the + /// > application should detect the network errors defined for the + /// > protocol after accept() and treat them like EAGAIN by retrying. + /// > In the case of TCP/IP, these are ENETDOWN, EPROTO, ENOPROTOOPT, + /// > EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, and ENETUNREACH. + /// Source: https://man7.org/linux/man-pages/man2/accept.2.html + /// + /// WASI implementations have two options to handle this: + /// - Optionally log it and then skip over non-fatal errors returned by + /// `accept`. Guest code never gets to see these failures. Or: + /// - Synthesize a `tcp-socket` resource that exposes the error when + /// attempting to send or receive on it. Guest code then sees these + /// failures as regular I/O errors. + /// + /// In either case, the stream returned by this `listen` method remains + /// operational. + /// + /// WASI requires `listen` to perform an implicit bind if the socket + /// has not already been bound. Not all platforms (notably Windows) + /// exhibit this behavior out of the box. On platforms that require it, + /// the WASI implementation can emulate this behavior by performing + /// the bind itself if the guest hasn't already done so. + /// + /// # References + /// - + /// - + /// - + /// - + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + listen: func() -> result, error-code>; + /// Transmit data to peer. + /// + /// The caller should close the stream when it has no more data to send + /// to the peer. Under normal circumstances this will cause a FIN packet + /// to be sent out. Closing the stream is equivalent to calling + /// `shutdown(SHUT_WR)` in POSIX. + /// + /// This function may be called at most once and returns once the full + /// contents of the stream are transmitted or an error is encountered. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + send: func(data: stream) -> future>; + /// Read data from peer. + /// + /// This function returns a `stream` which provides the data received from the + /// socket, and a `future` providing additional error information in case the + /// socket is closed abnormally. + /// + /// If the socket is closed normally, `stream.read` on the `stream` will return + /// `read-status::closed` with no `error-context` and the future resolves to + /// the value `ok`. If the socket is closed abnormally, `stream.read` on the + /// `stream` returns `read-status::closed` with an `error-context` and the future + /// resolves to `err` with an `error-code`. + /// + /// `receive` is meant to be called only once per socket. If it is called more + /// than once, the subsequent calls return a new `stream` that fails as if it + /// were closed abnormally. + /// + /// If the caller is not expecting to receive any data from the peer, + /// they may drop the stream. Any data still in the receive queue + /// will be discarded. This is equivalent to calling `shutdown(SHUT_RD)` + /// in POSIX. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + /// - `connection-reset`: The connection was reset. (ECONNRESET) + /// - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + receive: func() -> tuple, future>>; + /// 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 `get-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.3.0-rc-2026-02-09) + get-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.3.0-rc-2026-02-09) + get-remote-address: func() -> result; + /// Whether the socket is in the `listening` state. + /// + /// Equivalent to the SO_ACCEPTCONN socket option. + @since(version = 0.3.0-rc-2026-02-09) + get-is-listening: func() -> bool; + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0-rc-2026-02-09) + get-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 `connecting` or `connected` state. + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-keep-alive-enabled: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-keep-alive-idle-time: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-keep-alive-interval: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-keep-alive-count: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2026-02-09) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } + + /// A UDP socket handle. + @since(version = 0.3.0-rc-2026-02-09) + resource 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. + /// + /// Unlike POSIX, WASI sockets have no notion of a socket-level + /// `O_NONBLOCK` flag. Instead they fully rely on the Component Model's + /// async support. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + create: static func(address-family: ip-address-family) -> result; + /// Bind the socket to 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 can be bound to. (EADDRNOTAVAIL) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + bind: func(local-address: ip-socket-address) -> result<_, error-code>; + /// Associate this socket with a specific peer address. + /// + /// 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`. If the socket was not already explicitly + /// bound, this function will implicitly bind the socket to a random + /// free port. + /// + /// When a UDP socket is "connected", the `send` and `receive` methods + /// are limited to communicating with that peer only: + /// - `send` can only be used to send to this destination. + /// - `receive` will only return datagrams sent from the provided `remote-address`. + /// + /// The name "connect" was kept to align with the existing POSIX + /// terminology. Other than that, this function only changes the local + /// socket configuration and does not generate any network traffic. + /// The peer is not aware of this "connection". + /// + /// This method may be called multiple times on the same socket to change + /// its association, but only the most recent one will be effective. + /// + /// # 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) + /// - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + /// + /// # Implementors note + /// If the socket is already connected, some platforms (e.g. Linux) + /// require a disconnect before connecting to a different peer address. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + connect: func(remote-address: ip-socket-address) -> result<_, error-code>; + /// Dissociate this socket from its peer address. + /// + /// After calling this method, `send` & `receive` are free to communicate + /// with any address again. + /// + /// The POSIX equivalent of this is calling `connect` with an `AF_UNSPEC` address. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not connected. + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + disconnect: func() -> result<_, error-code>; + /// Send a message on the socket to a particular peer. + /// + /// If the socket is connected, the peer address may be left empty. In + /// that case this is equivalent to `send` in POSIX. Otherwise it is + /// equivalent to `sendto`. + /// + /// Additionally, if the socket is connected, a `remote-address` argument + /// _may_ be provided but then it must be identical to the address + /// passed to `connect`. + /// + /// Implementations may trap if the `data` length exceeds 64 KiB. + /// + /// # 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 `connect`. (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.3.0-rc-2026-02-09) + send: async func(data: list, remote-address: option) -> result<_, error-code>; + /// Receive a message on the socket. + /// + /// On success, the return value contains a tuple of the received data + /// and the address of the sender. Theoretical maximum length of the + /// data is 64 KiB. Though in practice, it will typically be less than + /// 1500 bytes. + /// + /// If the socket is connected, the sender address is guaranteed to + /// match the remote address passed to `connect`. + /// + /// # Typical errors + /// - `invalid-state`: The socket has not been bound yet. + /// - `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.3.0-rc-2026-02-09) + receive: async func() -> result, ip-socket-address>, 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 `get-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.3.0-rc-2026-02-09) + get-local-address: func() -> result; + /// Get the address the socket is currently "connected" to. + /// + /// # Typical errors + /// - `invalid-state`: The socket is not "connected" to a specific remote address. (ENOTCONN) + /// + /// # References + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + get-remote-address: func() -> result; + /// Whether this is a IPv4 or IPv6 socket. + /// + /// This is the value passed to the constructor. + /// + /// Equivalent to the SO_DOMAIN socket option. + @since(version = 0.3.0-rc-2026-02-09) + get-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.3.0-rc-2026-02-09) + get-unicast-hop-limit: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + 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.3.0-rc-2026-02-09) + get-receive-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + set-receive-buffer-size: func(value: u64) -> result<_, error-code>; + @since(version = 0.3.0-rc-2026-02-09) + get-send-buffer-size: func() -> result; + @since(version = 0.3.0-rc-2026-02-09) + set-send-buffer-size: func(value: u64) -> result<_, error-code>; + } +} + +@since(version = 0.3.0-rc-2026-02-09) +interface ip-name-lookup { + @since(version = 0.3.0-rc-2026-02-09) + use types.{ip-address}; + + /// Lookup error codes. + @since(version = 0.3.0-rc-2026-02-09) + enum error-code { + /// Unknown error + unknown, + /// Access denied. + /// + /// POSIX equivalent: EACCES, EPERM + access-denied, + /// `name` is a syntactically invalid domain name or IP address. + /// + /// POSIX equivalent: EINVAL + invalid-argument, + /// Name does not exist or has no suitable associated IP addresses. + /// + /// POSIX equivalent: EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY + name-unresolvable, + /// A temporary failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_AGAIN + temporary-resolver-failure, + /// A permanent failure in name resolution occurred. + /// + /// POSIX equivalent: EAI_FAIL + permanent-resolver-failure, + } + + /// 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. + /// + /// The results are returned in connection order preference. + /// + /// This function never succeeds with 0 results. It either fails or succeeds + /// with at least one address. Additionally, this function never returns + /// IPv4-mapped IPv6 addresses. + /// + /// The returned future will resolve to an error code in case of failure. + /// It will resolve to success once the returned stream is exhausted. + /// + /// # References: + /// - + /// - + /// - + /// - + @since(version = 0.3.0-rc-2026-02-09) + resolve-addresses: async func(name: string) -> result, error-code>; +} + +@since(version = 0.3.0-rc-2026-02-09) +world imports { + @since(version = 0.3.0-rc-2026-02-09) + import wasi:clocks/types@0.3.0-rc-2026-02-09; + @since(version = 0.3.0-rc-2026-02-09) + import types; + @since(version = 0.3.0-rc-2026-02-09) + import ip-name-lookup; +} diff --git a/packages/jco/test/fixtures/p3/wit/world.wit b/packages/jco/test/fixtures/p3/wit/world.wit index f51ee0a62..307b17fbf 100644 --- a/packages/jco/test/fixtures/p3/wit/world.wit +++ b/packages/jco/test/fixtures/p3/wit/world.wit @@ -10,8 +10,8 @@ interface commands { } world command-extended { - import wasi:cli/environment@0.3.0; - include wasi:http/proxy@0.3.0-draft; + import wasi:cli/environment@0.3.0-rc-2026-02-09; + include wasi:http/service@0.3.0-rc-2026-02-09; export commands; } diff --git a/packages/preview3-shim/lib/nodejs/cli.js b/packages/preview3-shim/lib/nodejs/cli.js index 69de872b7..534fd6100 100644 --- a/packages/preview3-shim/lib/nodejs/cli.js +++ b/packages/preview3-shim/lib/nodejs/cli.js @@ -3,6 +3,7 @@ import { Readable } from "node:stream"; import { ResourceWorker } from "./workers/resource-worker.js"; import { StreamReader } from "./stream.js"; +import { future } from "./future.js"; export { _appendEnv, @@ -26,53 +27,94 @@ function worker() { return (WORKER ??= new ResourceWorker(new URL("./workers/cli-worker.js", import.meta.url))); } +/** + * Map a Node.js error to a WIT error-code enum value. + * WIT: enum error-code { io, illegal-byte-sequence, pipe } + * @param {Error} err + * @returns {string} + */ +function errorCode(err) { + if (err?.code === "EPIPE" || err?.code === "ERR_STREAM_PREMATURE_CLOSE") { + return "pipe"; + } + if (err?.code === "ERR_ENCODING_INVALID_ENCODED_DATA") { + return "illegal-byte-sequence"; + } + return "io"; +} + export const stdin = { /** - * Creates a StreamReader that reads from standard input. + * Return a stream for reading from stdin, and a future to signal read results. * * WIT: * ``` - * get-stdin: func() -> stream; + * read-via-stream: func() -> tuple, future>>; * ``` * - * @returns {StreamReader} A reader for the process standard input. + * @returns {[StreamReader, FutureReader]} A tuple of [stream, future]. */ - getStdin() { - const stream = Readable.toWeb(process.stdin); - return new StreamReader(stream); + readViaStream() { + const readable = Readable.toWeb(process.stdin); + const { tx: futureTx, rx: futureRx } = future(); + + const reader = new StreamReader(readable); + const originalRead = reader.read.bind(reader); + reader.read = async () => { + try { + const chunk = await originalRead(); + if (chunk === null) { + await futureTx.write({ tag: "ok", val: undefined }); + } + return chunk; + } catch (err) { + await futureTx.write({ tag: "err", val: errorCode(err) }); + throw err; + } + }; + + return [reader, futureRx]; }, }; export const stdout = { /** - * Pipes the output of a StreamReader to standard output. + * Write the given stream to stdout. * * WIT: * ``` - * set-stdout: func(data: stream); + * write-via-stream: func(data: stream) -> future>; * ``` - * - * @param {StreamReader} streamReader - The reader whose output will be written to stdout. + * @returns {Promise<{tag: string, val?: string}>} Result of the write operation. */ - setStdout(streamReader) { - const stream = streamReader.intoReadableStream(); - worker().run({ op: "stdout", stream }, [stream]); + async writeViaStream(streamReader) { + const readableStream = streamReader.intoReadableStream(); + try { + await worker().run({ op: "stdout", stream: readableStream }, [readableStream]); + return { tag: "ok", val: undefined }; + } catch (err) { + return { tag: "err", val: errorCode(err) }; + } }, }; export const stderr = { /** - * Pipes the output of a StreamReader to standard error. + * Write the given stream to stderr. * * WIT: * ``` - * set-stderr: func(data: stream); + * write-via-stream: func(data: stream) -> future>; * ``` - * - * @param {StreamReader} streamReader - The reader whose output will be written to stderr. + * @returns {Promise<{tag: string, val?: string}>} Result of the write operation. */ - setStdout(streamReader) { - const stream = streamReader.intoReadableStream(); - worker().run({ op: "stderr", stream }, [stream]); + async writeViaStream(streamReader) { + const readableStream = streamReader.intoReadableStream(); + try { + await worker().run({ op: "stderr", stream: readableStream }, [readableStream]); + return { tag: "ok", val: undefined }; + } catch (err) { + return { tag: "err", val: errorCode(err) }; + } }, }; diff --git a/packages/preview3-shim/lib/nodejs/clocks.js b/packages/preview3-shim/lib/nodejs/clocks.js index 8d74e8d0f..b1be71e64 100644 --- a/packages/preview3-shim/lib/nodejs/clocks.js +++ b/packages/preview3-shim/lib/nodejs/clocks.js @@ -1,5 +1,27 @@ -export { wallClock } from "@bytecodealliance/preview2-shim/clocks"; -import { monotonicClock as monotonicClockV2 } from "@bytecodealliance/preview2-shim/clocks"; +import { + wallClock, + monotonicClock as monotonicClockV2, +} from "@bytecodealliance/preview2-shim/clocks"; + +// Re-export wall-clock as system-clock per latest WASI 0.3.0-draft. +export const systemClock = { + now: wallClock.now, + + /** + * Query the resolution of the clock. + * + * WIT: + * ``` + * get-resolution: func() -> duration; + * ``` + * + * @returns {bigint} The smallest distinguishable duration in nanoseconds. + */ + getResolution() { + const dt = wallClock.resolution(); + return BigInt(dt.seconds) * 1_000_000_000n + BigInt(dt.nanoseconds); + }, +}; // eslint-disable-next-line no-unused-vars const { subscribeInstant, subscribeDuration, ...baseMonotonicClock } = monotonicClockV2; @@ -10,15 +32,29 @@ export const monotonicClock = { ...baseMonotonicClock, /** - * Waits until a specific target time (in nanoseconds) is reached + * Query the resolution of the clock. * * WIT: * ``` - * wait-until: async func(when: instant); + * get-resolution: func() -> duration; + * ``` + * + * @returns {bigint} Duration of a clock tick in nanoseconds. + */ + getResolution() { + return baseMonotonicClock.resolution(); + }, + + /** + * Waits until a specific mark (in nanoseconds) is reached + * + * WIT: + * ``` + * wait-until: async func(when: mark); * ``` * @async - * @param {bigint} targetNs - The target time in nanoseconds to wait until - * @returns {Promise} A promise that resolves when the target time is reached + * @param {bigint} targetNs - The target mark in nanoseconds to wait until + * @returns {Promise} A promise that resolves when the target mark is reached * @throws {TypeError} If targetNs is not a bigint */ async waitUntil(targetNs) { @@ -50,6 +86,10 @@ export const monotonicClock = { * @throws {TypeError} If durationNs is not a bigint */ async waitFor(durationNs) { + if (durationNs <= 0n) { + return; + } + const ms = durationNs / 1_000_000n; if (ms > BigInt(Number.MAX_SAFE_INTEGER)) { throw new TypeError(`Cannot wait for ${durationNs} ns, exceeds maximum safe integer`); diff --git a/packages/preview3-shim/lib/nodejs/filesystem/descriptor.js b/packages/preview3-shim/lib/nodejs/filesystem/descriptor.js index 6bfa6e4a5..a6525f2e7 100644 --- a/packages/preview3-shim/lib/nodejs/filesystem/descriptor.js +++ b/packages/preview3-shim/lib/nodejs/filesystem/descriptor.js @@ -36,7 +36,7 @@ class Descriptor { write = false, fileIntegritySync = false, dataIntegritySync = false, - requestedIntegritySync = false, + requestedWriteSync = false, mutateDirectory = false, } = mode; @@ -45,7 +45,7 @@ class Descriptor { write, fileIntegritySync, dataIntegritySync, - requestedIntegritySync, + requestedWriteSync, mutateDirectory, }; @@ -114,10 +114,7 @@ class Descriptor { throw FSError.from(err); }); - return { - stream: new StreamReader(transform.readable), - future: new FutureReader(promise), - }; + return [new StreamReader(transform.readable), new FutureReader(promise)]; } /** @@ -334,10 +331,7 @@ class Descriptor { throw FSError.from(err); }); - return { - stream: new StreamReader(transform.readable), - future: new FutureReader(promise), - }; + return [new StreamReader(transform.readable), new FutureReader(promise)]; } /** @@ -574,7 +568,7 @@ class Descriptor { if (df.fileIntegritySync) { fsFlags |= fs.constants.O_SYNC; } - if (df.requestedIntegritySync) { + if (df.requestedWriteSync) { fsFlags |= fs.constants.O_SYNC; } if (df.dataIntegritySync) { @@ -625,7 +619,7 @@ class Descriptor { return desc; } catch (err) { if (err.code === "ERR_INVALID_ARG_VALUE") { - throw process.platform === "win32" ? "no-entry" : "invalid"; + throw new FSError(process.platform === "win32" ? "no-entry" : "invalid"); } throw FSError.from(err); } @@ -791,7 +785,7 @@ class Descriptor { * @param {Descriptor} other Another descriptor. * @returns {Promise} */ - isSameObject(other) { + async isSameObject(other) { return other === this; } diff --git a/packages/preview3-shim/lib/nodejs/http/client.js b/packages/preview3-shim/lib/nodejs/http/client.js index 5717cbbea..445d28dd0 100644 --- a/packages/preview3-shim/lib/nodejs/http/client.js +++ b/packages/preview3-shim/lib/nodejs/http/client.js @@ -1,8 +1,9 @@ import { ResourceWorker } from "../workers/resource-worker.js"; import { StreamReader } from "../stream.js"; -import { FutureReader } from "../future.js"; +import { FutureReader, future } from "../future.js"; import { _fieldsFromEntriesChecked } from "./fields.js"; import { HttpError } from "./error.js"; +import { Request } from "./request.js"; import { Response } from "./response.js"; let WORKER = null; @@ -10,20 +11,31 @@ function worker() { return (WORKER ??= new ResourceWorker(new URL("../workers/http-worker.js", import.meta.url))); } -export class HttpClient { +/** + * HTTP client interface. + * + * WIT: + * ``` + * interface client { + * use types.{request, response, error-code}; + * send: async func(request) -> result; + * } + * ``` + */ +export const client = { /** - * Send a Request, return a fully-formed Response object + * Send an HTTP request and return a response. * * @param {Request} req * @returns {Promise} * @throws {HttpError} */ - static async request(req) { + async send(req) { const scheme = req.scheme() ?? "http"; const authority = req.authority(); if (!authority) { - throw new HttpError("internal-error", "Request.authority must be set for client.request"); + throw new HttpError("internal-error", "Request.authority must be set for client.send"); } const path = req.pathWithQuery() ?? "/"; @@ -34,7 +46,8 @@ export class HttpClient { const firstByteTimeoutNs = opts?.firstByteTimeout() ?? null; const betweenBytesTimeoutNs = opts?.betweenBytesTimeout() ?? null; - const { body, trailers } = req.body(); + const { rx: resRx } = future(); + const [body, trailers] = Request.consumeBody(req, resRx); const { port1: tx, port2: rx } = new MessageChannel(); const transfer = [rx]; @@ -71,8 +84,8 @@ export class HttpClient { } catch (err) { throw HttpError.from(err); } - } -} + }, +}; const responseFromParts = (parts) => { const { headers, body, trailers, statusCode } = parts; @@ -93,7 +106,7 @@ const responseFromParts = (parts) => { const contents = new StreamReader(body); const fields = _fieldsFromEntriesChecked(headers); - const { res } = Response.new(fields, contents, future); + const [res] = Response.new(fields, contents, future); res.setStatusCode(statusCode); return res; }; diff --git a/packages/preview3-shim/lib/nodejs/http/request.js b/packages/preview3-shim/lib/nodejs/http/request.js index c8c30e7e3..6972ab242 100644 --- a/packages/preview3-shim/lib/nodejs/http/request.js +++ b/packages/preview3-shim/lib/nodejs/http/request.js @@ -197,7 +197,7 @@ export class Request { * @param {?StreamReader} contents optional body stream * @param {FutureReader} trailers future for trailers * @param {?RequestOptions} options optional RequestOptions - * @returns {{ req: Request, future: FutureReader }} + * @returns {[Request, FutureReader]} * @throws {HttpError} with payload.tag 'invalid-argument' for invalid arguments * */ @@ -233,7 +233,7 @@ export class Request { const { tx, rx } = future(); request.#requestFuture = tx; - return { req: request, future: rx }; + return [request, rx]; } /** @@ -395,39 +395,42 @@ export class Request { } /** - * Get the body stream and trailers future. + * Get body stream and trailers future, consuming the resource. * * WIT: * ``` - * body: func() -> result, future, error-code>>>>; + * consume-body: static func(this: request, res: future>) + * -> tuple, future, error-code>>>; * ``` * - * @returns {{ req: StreamReader, trailers: FutureReader }} + * @param {Request} request - The request to consume. + * @param {FutureReader} res - A future for communicating errors. + * @returns {[StreamReader, FutureReader]} A tuple of [body stream, trailers future]. * @throws {HttpError} with payload.tag 'invalid-state' if body already open or consumed. */ - body() { - if (this.#bodyOpen) { - throw new HttpError("invalid-state", "body() already called and not yet closed"); + static consumeBody(request, _res) { + if (request.#bodyOpen) { + throw new HttpError("invalid-state", "body already called and not yet closed"); } - if (this.#bodyEnded) { - throw new HttpError("invalid-state", "body() has already been consumed"); + if (request.#bodyEnded) { + throw new HttpError("invalid-state", "body has already been consumed"); } - if (!this.#contents) { - this.#bodyEnded = true; - return { body: this.#contents, trailers: this.#trailersFuture }; + if (!request.#contents) { + request.#bodyEnded = true; + return [request.#contents, request.#trailersFuture]; } - const reader = this.#contents; - this.#bodyOpen = true; + const reader = request.#contents; + request.#bodyOpen = true; const readFn = reader.read.bind(reader); reader.read = () => { const chunk = readFn(); if (chunk === null) { - this.#bodyEnded = true; - this.#bodyOpen = false; + request.#bodyEnded = true; + request.#bodyOpen = false; } return chunk; @@ -436,11 +439,11 @@ export class Request { const closedFn = reader.close.bind(reader); reader.close = () => { closedFn(); - this.#bodyEnded = true; - this.#bodyOpen = false; + request.#bodyEnded = true; + request.#bodyOpen = false; }; - return { body: this.#contents, trailers: this.#trailersFuture }; + return [request.#contents, request.#trailersFuture]; } // TODO: placeholder diff --git a/packages/preview3-shim/lib/nodejs/http/response.js b/packages/preview3-shim/lib/nodejs/http/response.js index ee91894d2..a26c5b8a7 100644 --- a/packages/preview3-shim/lib/nodejs/http/response.js +++ b/packages/preview3-shim/lib/nodejs/http/response.js @@ -66,7 +66,7 @@ export class Response { const { tx, rx } = future(); response.#responseFuture = tx; - return { res: response, future: rx }; + return [response, rx]; } /** @@ -117,39 +117,42 @@ export class Response { } /** - * Get the body stream and trailers future. + * Get body stream and trailers future, consuming the resource. * * WIT: * ``` - * body: func() -> result, future, error-code>>>>; + * consume-body: static func(this: response, res: future>) + * -> tuple, future, error-code>>>; * ``` * - * @returns {{ body: StreamReader, trailers: FutureReader }} + * @param {Response} response - The response to consume. + * @param {FutureReader} res - A future for communicating errors. + * @returns {[StreamReader, FutureReader]} A tuple of [body stream, trailers future]. * @throws {HttpError} with payload.tag 'invalid-state' if body has already been opened or consumed. */ - body() { - if (this.#bodyOpen) { - throw new HttpError("invalid-state", "body() already opened and not yet closed"); + static consumeBody(response, _res) { + if (response.#bodyOpen) { + throw new HttpError("invalid-state", "body already opened and not yet closed"); } - if (this.#bodyEnded) { - throw new HttpError("invalid-state", "body() has already been consumed"); + if (response.#bodyEnded) { + throw new HttpError("invalid-state", "body has already been consumed"); } - if (!this.#contents) { - this.#bodyEnded = true; - return { body: this.#contents, trailers: this.#trailersFuture }; + if (!response.#contents) { + response.#bodyEnded = true; + return [response.#contents, response.#trailersFuture]; } - const reader = this.#contents; - this.#bodyOpen = true; + const reader = response.#contents; + response.#bodyOpen = true; const readFn = reader.read.bind(reader); reader.read = () => { const chunk = readFn(); if (chunk === null) { - this.#bodyEnded = true; - this.#bodyOpen = false; + response.#bodyEnded = true; + response.#bodyOpen = false; } return chunk; @@ -158,11 +161,11 @@ export class Response { const closedFn = reader.close.bind(reader); reader.close = () => { closedFn(); - this.#bodyEnded = true; - this.#bodyOpen = false; + response.#bodyEnded = true; + response.#bodyOpen = false; }; - return { body: this.#contents, trailers: this.#trailersFuture }; + return [response.#contents, response.#trailersFuture]; } // Internal: call to complete the response transmission diff --git a/packages/preview3-shim/lib/nodejs/http/server.js b/packages/preview3-shim/lib/nodejs/http/server.js index f89df3a26..9406bdcdd 100644 --- a/packages/preview3-shim/lib/nodejs/http/server.js +++ b/packages/preview3-shim/lib/nodejs/http/server.js @@ -2,8 +2,9 @@ import { EventEmitter } from "node:events"; import { ResourceWorker } from "../workers/resource-worker.js"; import { StreamReader } from "../stream.js"; -import { FutureReader } from "../future.js"; +import { FutureReader, future } from "../future.js"; import { Request } from "./request.js"; +import { Response } from "./response.js"; import { HttpError } from "./error.js"; import { _fieldsFromEntriesChecked } from "./fields.js"; @@ -27,7 +28,7 @@ export class HttpServer extends EventEmitter { super(); if (typeof handler?.handle !== "function") { - throw HttpError("invalid-argument", "Not a valid HTTP server component to execute."); + throw new HttpError("invalid-argument", "Not a valid HTTP server component to execute."); } this.#handler = handler; } @@ -87,7 +88,8 @@ export class HttpServer extends EventEmitter { if (outcome.tag === "ok") { const res = outcome.val; - const { body, trailers } = res.body(); + const { rx: resRx } = future(); + const [body, trailers] = Response.consumeBody(res, resRx); const { port1: tx, port2: rx } = new MessageChannel(); const stream = body.intoReadableStream(); @@ -146,7 +148,7 @@ const requestFromParts = (parts) => { const contents = new StreamReader(body); const fields = _fieldsFromEntriesChecked(headers); - const { req } = Request.new(fields, contents, future, null); + const [req] = Request.new(fields, contents, future, null); req.setMethod(method); req.setPathWithQuery(url); return req; diff --git a/packages/preview3-shim/lib/nodejs/sockets/address.js b/packages/preview3-shim/lib/nodejs/sockets/address.js index a2cba0973..72855ab38 100644 --- a/packages/preview3-shim/lib/nodejs/sockets/address.js +++ b/packages/preview3-shim/lib/nodejs/sockets/address.js @@ -25,12 +25,24 @@ export const ipv6ToTuple = (ipv6) => { const lhsParts = lhs === "" ? [] : lhs.split(":"); const rhsParts = rhs === "" ? [] : rhs.split(":"); return [...lhsParts, ...Array(8 - lhsParts.length - rhsParts.length).fill(0), ...rhsParts].map( - (segment) => parseInt(segment, 16), + (segment) => { + const val = parseInt(segment, 16); + if (val < 0 || val > 0xffff || Number.isNaN(val)) { + throw new Error(`Invalid IPv6 segment: ${segment}`); + } + return val; + }, ); }; export const ipv4ToTuple = (ipv4) => { - return ipv4.split(".").map((segment) => parseInt(segment, 10)); + return ipv4.split(".").map((segment) => { + const val = parseInt(segment, 10); + if (val < 0 || val > 255 || Number.isNaN(val)) { + throw new Error(`Invalid IPv4 segment: ${segment}`); + } + return val; + }); }; export const isMulticastIpAddress = ({ tag, val: { address } }) => diff --git a/packages/preview3-shim/lib/nodejs/sockets/tcp.js b/packages/preview3-shim/lib/nodejs/sockets/tcp.js index b21ad4531..9968cdaf5 100644 --- a/packages/preview3-shim/lib/nodejs/sockets/tcp.js +++ b/packages/preview3-shim/lib/nodejs/sockets/tcp.js @@ -277,7 +277,7 @@ export class TcpSocket { * receive: func() -> tuple, future>> * ``` * - * @throws {SocketError} with payload.tag 'invalid-state' if sockt is not CONNECTED + * @throws {SocketError} with payload.tag 'invalid-state' if socket is not CONNECTED * @throws {SocketError} for other errors, payload.tag maps the system error * @returns {{ stream: StreamReader, future: FutureReader }} */ @@ -300,10 +300,7 @@ export class TcpSocket { throw SocketError.from(err); }); - return { - stream: new StreamReader(transform.readable), - future: new FutureReader(promise), - }; + return [new StreamReader(transform.readable), new FutureReader(promise)]; } /** @@ -314,7 +311,7 @@ export class TcpSocket { * ``` * * @returns {Object} The local socket address - * @throws {SocketError} with payload.tag 'invalid-state' if sockt is not BOUND + * @throws {SocketError} with payload.tag 'invalid-state' if socket is not BOUND * @throws {SocketError} for other errors, payload.tag maps the system error */ localAddress() { @@ -340,7 +337,7 @@ export class TcpSocket { * ``` * * @returns {Object} The remote socket address - * @throws {SocketError} with payload.tag 'invalid-state' if sockt is not CONNECTED + * @throws {SocketError} with payload.tag 'invalid-state' if socket is not CONNECTED * @throws {SocketError} for other errors, payload.tag maps the system error */ remoteAddress() { diff --git a/packages/preview3-shim/lib/nodejs/sockets/udp.js b/packages/preview3-shim/lib/nodejs/sockets/udp.js index eceaea18b..61032ac87 100644 --- a/packages/preview3-shim/lib/nodejs/sockets/udp.js +++ b/packages/preview3-shim/lib/nodejs/sockets/udp.js @@ -212,11 +212,17 @@ export class UdpSocket { } const addr = remoteAddress ?? this.#remote; - if (!addr || addr.val.port === 0n || addr.tag !== this.#family) { + if (!addr || addr.val.port === 0 || addr.tag !== this.#family) { throw new SocketError("invalid-argument"); } - if (this.#state === STATE.CONNECTED && addr !== this.#remote) { + if ( + this.#state === STATE.CONNECTED && + remoteAddress != null && + (addr.tag !== this.#remote.tag || + addr.val.port !== this.#remote.val.port || + addr.val.address.some((v, i) => v !== this.#remote.val.address[i])) + ) { throw new SocketError("invalid-argument"); } diff --git a/packages/preview3-shim/lib/nodejs/stream.js b/packages/preview3-shim/lib/nodejs/stream.js index 8bf79932a..ec13e3be2 100644 --- a/packages/preview3-shim/lib/nodejs/stream.js +++ b/packages/preview3-shim/lib/nodejs/stream.js @@ -192,8 +192,9 @@ export class StreamWriter { */ async close() { this.#ensureWriter(); - await this.#writer.close(); + const writer = this.#writer; this.#writer = null; + await writer.close(); } /** @@ -204,8 +205,9 @@ export class StreamWriter { */ async closeWithError(error) { this.#ensureWriter(); - await this.#writer.abort(error); + const writer = this.#writer; this.#writer = null; + await writer.abort(error); } /** diff --git a/packages/preview3-shim/lib/nodejs/workers/filesystem-worker.js b/packages/preview3-shim/lib/nodejs/workers/filesystem-worker.js index a7602f728..46ddaefea 100644 --- a/packages/preview3-shim/lib/nodejs/workers/filesystem-worker.js +++ b/packages/preview3-shim/lib/nodejs/workers/filesystem-worker.js @@ -85,6 +85,7 @@ async function handleReadDir({ fullPath, stream, preopens }) { throw new TypeError("stream must have a getWriter() method"); } + const writer = stream.getWriter(); const canAccess = preopens.some((p) => fullPath === p || fullPath.startsWith(p + "/")); if (!canAccess) { @@ -93,8 +94,6 @@ async function handleReadDir({ fullPath, stream, preopens }) { throw err; } - const writer = stream.getWriter(); - try { const walker = await opendir(fullPath); for await (const dirent of walker) { diff --git a/packages/preview3-shim/lib/nodejs/workers/http-worker.js b/packages/preview3-shim/lib/nodejs/workers/http-worker.js index 2c7ed5d14..91b476c51 100644 --- a/packages/preview3-shim/lib/nodejs/workers/http-worker.js +++ b/packages/preview3-shim/lib/nodejs/workers/http-worker.js @@ -199,7 +199,7 @@ async function handleRequest({ url, method, headers, trailers, body, timeouts }) }; const onClose = () => { cleanup(); - reject(new HttpError("connecetion-terminated")); + reject(new HttpError("connection-terminated")); }; const cleanup = () => { diff --git a/packages/preview3-shim/test/cli.test.js b/packages/preview3-shim/test/cli.test.js index 89578f43b..c3839ad81 100644 --- a/packages/preview3-shim/test/cli.test.js +++ b/packages/preview3-shim/test/cli.test.js @@ -4,7 +4,7 @@ import process from "node:process"; import { Readable } from "node:stream"; describe("Node.js Preview3 wasi-cli", () => { - test("setStdout to Readable stream", async () => { + test("writeViaStream writes to stdout", async () => { const { cli } = await import("@bytecodealliance/preview3-shim"); const { stream } = await import("@bytecodealliance/preview3-shim/stream"); @@ -23,19 +23,21 @@ describe("Node.js Preview3 wasi-cli", () => { }); const { tx, rx } = stream(); - cli.stdout.setStdout(rx); + const resultPromise = cli.stdout.writeViaStream(rx); const message = "Hello world!"; await tx.write(message); await tx.close(); await finished; + const result = await resultPromise; process.stdout.write = restore; expect(output).toBe(message); + expect(result.tag).toBe("ok"); }); - test("getStdin returns a StreamReader", async () => { + test("readViaStream returns [stream, future] tuple", async () => { const { cli } = await import("@bytecodealliance/preview3-shim"); const input = "Hello, stdin!"; @@ -47,7 +49,7 @@ describe("Node.js Preview3 wasi-cli", () => { configurable: true, }); - const streamReader = cli.stdin.getStdin(); + const [streamReader] = cli.stdin.readViaStream(); const result = await streamReader.read(); expect(result).toBe(input); @@ -55,6 +57,38 @@ describe("Node.js Preview3 wasi-cli", () => { Object.defineProperty(process, "stdin", { value: originalStdin }); }); + test("writeViaStream writes to stderr", async () => { + const { cli } = await import("@bytecodealliance/preview3-shim"); + const { stream } = await import("@bytecodealliance/preview3-shim/stream"); + + const restore = process.stderr.write.bind(process.stderr); + let output = ""; + + const finished = new Promise((resolve) => { + process.stderr.write = (chunk, _enc, cb) => { + output += chunk; + cb?.(); + if (output === message) { + resolve(); + } + }; + }); + + const { tx, rx } = stream(); + const resultPromise = cli.stderr.writeViaStream(rx); + + const message = "error output"; + await tx.write(message); + await tx.close(); + + await finished; + const result = await resultPromise; + + process.stderr.write = restore; + expect(output).toBe(message); + expect(result.tag).toBe("ok"); + }); + test("Overriding previously set stdout", async () => { const { cli } = await import("@bytecodealliance/preview3-shim"); const { stream } = await import("@bytecodealliance/preview3-shim/stream"); @@ -76,14 +110,14 @@ describe("Node.js Preview3 wasi-cli", () => { // first stream const { tx: tx1, rx: rx1 } = stream(); - cli.stdout.setStdout(rx1); + cli.stdout.writeViaStream(rx1); await tx1.write("Hello "); await tx1.close(); await new Promise((r) => setTimeout(r, 50)); // override stdout const { tx: tx2, rx: rx2 } = stream(); - cli.stdout.setStdout(rx2); + cli.stdout.writeViaStream(rx2); await tx2.write("world!"); // writing to old stream should now reject diff --git a/packages/preview3-shim/test/filesystem.test.js b/packages/preview3-shim/test/filesystem.test.js index 3ddedc504..35d0b2fdc 100644 --- a/packages/preview3-shim/test/filesystem.test.js +++ b/packages/preview3-shim/test/filesystem.test.js @@ -38,7 +38,7 @@ describe("Descriptor with os.tmpdir()", () => { const subpath = fileURLToPath(import.meta.url).slice(1); const child = await rootDescriptor.openAt({}, subpath, {}, {}); - const { stream, future } = child.readViaStream(0n); + const [stream, future] = child.readViaStream(0n); const buf = await stream.readAll(); const text = new TextDecoder().decode(buf); expect(text).toContain("UNIQUE STRING"); @@ -62,7 +62,7 @@ describe("Descriptor with os.tmpdir()", () => { await tx.close(); await child.writeViaStream(rx, 0); - const { stream: sr, future: fr } = child.readViaStream(0n); + const [sr, fr] = child.readViaStream(0n); const buf = await sr.readAll(); await fr.read(); @@ -96,7 +96,7 @@ describe("Descriptor with os.tmpdir()", () => { await child.appendViaStream(rx); } - const { stream: sr, future: fr } = child.readViaStream(0n); + const [sr, fr] = child.readViaStream(0n); const buf = await sr.readAll(); await fr.read(); @@ -108,7 +108,7 @@ describe("Descriptor with os.tmpdir()", () => { const dirDesc = await rootDescriptor.openAt({}, relBase, { directory: true }, { read: true }); const readDir = async () => { - const { stream, future } = dirDesc.readDirectory(); + const [stream, future] = dirDesc.readDirectory(); const entries = []; let entry; while ((entry = await stream.read()) !== null) { @@ -140,7 +140,7 @@ describe("Descriptor with os.tmpdir()", () => { { read: true, write: true }, ); - expect(child.isSameObject(child)).toBe(true); + expect(await child.isSameObject(child)).toBe(true); const h1 = await child.metadataHash(); const h2 = await rootDescriptor.metadataHashAt({ symlinkFollow: true }, sub); diff --git a/packages/preview3-shim/test/future.test.js b/packages/preview3-shim/test/future.test.js index 37d506f95..0b57f3bd9 100644 --- a/packages/preview3-shim/test/future.test.js +++ b/packages/preview3-shim/test/future.test.js @@ -16,4 +16,47 @@ describe("Node.js Preview3 canon future", () => { const noValue = await rx.read(); expect(noValue).toBeNull(); }); + + test("close resolves to null", async () => { + const { future } = await import("@bytecodealliance/preview3-shim/future"); + const { tx, rx } = future(); + + await tx.close(); + + const value = await rx.read(); + expect(value).toBeNull(); + }); + + test("abort rejects the reader", async () => { + const { future } = await import("@bytecodealliance/preview3-shim/future"); + const { tx, rx } = future(); + + const err = new Error("aborted"); + await tx.abort(err); + + await expect(rx.read()).rejects.toThrow("aborted"); + }); + + test("writer cannot write twice", async () => { + const { future } = await import("@bytecodealliance/preview3-shim/future"); + const { tx, rx } = future(); + + await tx.write("first"); + await expect(tx.write("second")).rejects.toThrow(); + + // reader still gets the first value + const value = await rx.read(); + expect(value).toBe("first"); + }); + + test("intoPromise returns the underlying promise", async () => { + const { future } = await import("@bytecodealliance/preview3-shim/future"); + const { tx, rx } = future(); + + await tx.write(42); + + const promise = rx.intoPromise(); + expect(promise).toBeInstanceOf(Promise); + expect(await promise).toBe(42); + }); }); diff --git a/packages/preview3-shim/test/http/client.test.js b/packages/preview3-shim/test/http/client.test.js index 09e66325e..fa696cdf1 100644 --- a/packages/preview3-shim/test/http/client.test.js +++ b/packages/preview3-shim/test/http/client.test.js @@ -3,7 +3,13 @@ import http from "node:http"; import { describe, test, expect, afterAll, beforeAll } from "vitest"; -import { Fields, HttpClient, RequestOptions, Request } from "@bytecodealliance/preview3-shim/http"; +import { + Fields, + client, + RequestOptions, + Request, + Response, +} from "@bytecodealliance/preview3-shim/http"; import { future } from "@bytecodealliance/preview3-shim/future"; import { stream } from "@bytecodealliance/preview3-shim/stream"; @@ -64,7 +70,7 @@ describe("HttpClient Integration", () => { headers.append("accept", ENCODER.encode("application/json")); const { tx: trailersTx, rx: trailersRx } = future(); - const { req } = Request.new(headers, null, trailersRx); + const [req] = Request.new(headers, null, trailersRx); req.setMethod("GET"); req.setAuthority(authority); @@ -72,7 +78,7 @@ describe("HttpClient Integration", () => { trailersTx.write(null); - const response = await HttpClient.request(req); + const response = await client.send(req); expect(response.statusCode()).toBe(200); const responseHeaders = response.headers(); @@ -85,7 +91,8 @@ describe("HttpClient Integration", () => { checkHeader("content-type", "application/json"); checkHeader("x-test-header", "test-value"); - const { body } = response.body(); + const { rx: resRx2 } = future(); + const [body] = Response.consumeBody(response, resRx2); const stream = body.intoReadableStream(); const reader = stream.getReader(); let result = ""; @@ -110,7 +117,7 @@ describe("HttpClient Integration", () => { const { tx: bodyTx, rx: bodyRx } = stream(); const { tx: trailersTx, rx: trailersRx } = future(); - const { req } = Request.new(headers, bodyRx, trailersRx); + const [req] = Request.new(headers, bodyRx, trailersRx); req.setMethod("POST"); req.setAuthority(authority); req.setPathWithQuery("/submit"); @@ -120,7 +127,7 @@ describe("HttpClient Integration", () => { email: "test@example.com", }); - const responsePromise = HttpClient.request(req); + const responsePromise = client.send(req); await bodyTx.write(Buffer.from(requestData)); await bodyTx.close(); @@ -129,7 +136,8 @@ describe("HttpClient Integration", () => { const response = await responsePromise; expect(response.statusCode()).toBe(200); - const { body } = response.body(); + const { rx: resRx2 } = future(); + const [body] = Response.consumeBody(response, resRx2); const reader = body.intoReadableStream().getReader(); let result = ""; @@ -151,17 +159,18 @@ describe("HttpClient Integration", () => { const headers = new Fields(); const { tx: trailersTx, rx: trailersRx } = future(); - const { req } = Request.new(headers, null, trailersRx); + const [req] = Request.new(headers, null, trailersRx); req.setMethod("GET"); req.setAuthority(authority); req.setPathWithQuery("/error"); await trailersTx.write(null); - const response = await HttpClient.request(req); + const response = await client.send(req); expect(response.statusCode()).toBe(500); - const { body } = response.body(); + const { rx: resRx2 } = future(); + const [body] = Response.consumeBody(response, resRx2); const stream = body.intoReadableStream(); const reader = stream.getReader(); let result = ""; @@ -182,12 +191,12 @@ describe("HttpClient Integration", () => { const headers = new Fields(); const { tx: trailersTx, rx: trailersRx } = future(); - const { req } = Request.new(headers, null, trailersRx); + const [req] = Request.new(headers, null, trailersRx); req.setMethod("GET"); await trailersTx.write(null); - await expect(HttpClient.request(req)).rejects.toThrow("Request.authority must be set"); + await expect(client.send(req)).rejects.toThrow("Request.authority must be set"); }); test("first-byte timeout triggers HttpError", async () => { @@ -198,13 +207,13 @@ describe("HttpClient Integration", () => { opts.setFirstByteTimeout(1_000_000n); // 1 ms opts.setBetweenBytesTimeout(1_000_000n); // 1 ms - const { req } = Request.new(headers, null, trailersRx, opts); + const [req] = Request.new(headers, null, trailersRx, opts); req.setMethod("GET"); req.setAuthority(authority); req.setPathWithQuery("/delayed-first-byte"); await trailersTx.write(null); - await expect(HttpClient.request(req)).rejects.toThrow(/connection-timeout/); + await expect(client.send(req)).rejects.toThrow(/connection-timeout/); }); }); diff --git a/packages/preview3-shim/test/http/request.test.js b/packages/preview3-shim/test/http/request.test.js index c3de0c3fd..6a2b3638e 100644 --- a/packages/preview3-shim/test/http/request.test.js +++ b/packages/preview3-shim/test/http/request.test.js @@ -22,7 +22,7 @@ describe("Request", () => { }); test("new() returns Request and FutureReader with defaults", () => { - const { req, future } = Request.new(headers, contents, trailers, options); + const [req, future] = Request.new(headers, contents, trailers, options); expect(req).toBeInstanceOf(Request); expect(future).toBeInstanceOf(FutureReader); expect(req.method()).toBe("get"); @@ -32,7 +32,7 @@ describe("Request", () => { }); test("setMethod accepts standard and custom methods", () => { - const { req } = Request.new(headers, contents, trailers, options); + const [req] = Request.new(headers, contents, trailers, options); req.setMethod("POST"); expect(req.method()).toEqual({ tag: "post" }); @@ -42,7 +42,7 @@ describe("Request", () => { }); test("setMethod rejects invalid syntax", () => { - const { req } = Request.new(headers, contents, trailers, options); + const [req] = Request.new(headers, contents, trailers, options); expect(() => req.setMethod("BAD METHOD")).toThrowError(HttpError); expect(() => req.setMethod("BAD METHOD")).toThrowError( expect.objectContaining({ payload: { tag: "invalid-syntax" } }), @@ -50,7 +50,7 @@ describe("Request", () => { }); test("setScheme handles valid and invalid schemes", () => { - const { req } = Request.new(headers, contents, trailers, options); + const [req] = Request.new(headers, contents, trailers, options); req.setScheme("https"); expect(req.scheme()).toBe("https"); @@ -64,7 +64,7 @@ describe("Request", () => { }); test("setAuthority handles valid and invalid authorities", () => { - const { req } = Request.new(headers, contents, trailers, options); + const [req] = Request.new(headers, contents, trailers, options); req.setAuthority("example.com:8080"); expect(req.authority()).toBe("example.com:8080"); @@ -78,7 +78,7 @@ describe("Request", () => { }); test("headers() and options() are immutable", () => { - const { req } = Request.new(headers, contents, trailers, options); + const [req] = Request.new(headers, contents, trailers, options); expect(() => req.headers().append("x", new Uint8Array([0]))).toThrowError( expect.objectContaining({ payload: { tag: "immutable" } }), @@ -90,15 +90,17 @@ describe("Request", () => { }); }); -describe("Request.body single-stream semantics", () => { - test("throws if body() called twice without closing", async () => { +describe("Request.consumeBody single-stream semantics", () => { + test("throws if consumeBody() called twice without closing", async () => { const headers = new Fields(); const { tx: bodyTx, rx: bodyRx } = stream(); const { tx: trailersTx, rx: trailersRx } = future(); - const { req } = Request.new(headers, bodyRx, trailersRx); + const [req] = Request.new(headers, bodyRx, trailersRx); - req.body(); - expect(() => req.body()).toThrowError(HttpError); + const { rx: resRx2 } = future(); + Request.consumeBody(req, resRx2); + const { rx: resRx3 } = future(); + expect(() => Request.consumeBody(req, resRx3)).toThrowError(HttpError); await bodyTx.close(); await trailersTx.write(null); @@ -108,14 +110,16 @@ describe("Request.body single-stream semantics", () => { const headers = new Fields(); const { tx: bodyTx, rx: bodyRx } = stream(); const { tx: trailersTx, rx: trailersRx } = future(); - const { req } = Request.new(headers, bodyRx, trailersRx); + const [req] = Request.new(headers, bodyRx, trailersRx); - const { body } = req.body(); + const { rx: resRx2 } = future(); + const [body] = Request.consumeBody(req, resRx2); await bodyTx.write(Buffer.from("data")); await bodyTx.close(); await trailersTx.write(null); while ((await body.read()) !== null) {} - expect(() => req.body()).toThrowError(HttpError); + const { rx: resRx3 } = future(); + expect(() => Request.consumeBody(req, resRx3)).toThrowError(HttpError); }); }); diff --git a/packages/preview3-shim/test/http/response.test.js b/packages/preview3-shim/test/http/response.test.js index 684d21dd8..e93bae86c 100644 --- a/packages/preview3-shim/test/http/response.test.js +++ b/packages/preview3-shim/test/http/response.test.js @@ -31,7 +31,7 @@ describe("Response", () => { }); test("new() returns Response and FutureReader", () => { - const { res, future } = Response.new(headers, contents, trailers); + const [res, future] = Response.new(headers, contents, trailers); expect(res).toBeInstanceOf(Response); expect(future).toBeInstanceOf(FutureReader); expect(res.statusCode()).toBe(200); // Default status code @@ -55,17 +55,17 @@ describe("Response", () => { }); test("contents can be null", () => { - const { res } = Response.new(headers, null, trailers); + const [res] = Response.new(headers, null, trailers); expect(res).toBeInstanceOf(Response); }); test("statusCode() returns the current status code", () => { - const { res } = Response.new(headers, contents, trailers); + const [res] = Response.new(headers, contents, trailers); expect(res.statusCode()).toBe(200); // Default }); test("setStatusCode() changes the status code", () => { - const { res } = Response.new(headers, contents, trailers); + const [res] = Response.new(headers, contents, trailers); res.setStatusCode(404); expect(res.statusCode()).toBe(404); @@ -74,7 +74,7 @@ describe("Response", () => { }); test("setStatusCode() validates the status code", () => { - const { res } = Response.new(headers, contents, trailers); + const [res] = Response.new(headers, contents, trailers); expect(() => res.setStatusCode("200")).toThrow(HttpError); expect(() => res.setStatusCode(200.5)).toThrow(HttpError); @@ -83,7 +83,7 @@ describe("Response", () => { }); test("headers() returns the immutable headers", () => { - const { res } = Response.new(headers, contents, trailers); + const [res] = Response.new(headers, contents, trailers); const respHeaders = res.headers(); expect(respHeaders).toBe(headers); @@ -92,16 +92,17 @@ describe("Response", () => { ); }); - test("body() returns the contents stream and trailers future", () => { - const { res } = Response.new(headers, contents, trailers); - const { body, trailers: t } = res.body(); + test("consumeBody() returns the contents stream and trailers future", () => { + const [res] = Response.new(headers, contents, trailers); + const { rx: resRx2 } = future(); + const [body, t] = Response.consumeBody(res, resRx2); expect(body).toBe(contents); expect(t).toBe(trailers); }); test("_resolve method completes the response future", async () => { - const { res, future } = Response.new(headers, contents, trailers); + const [res, future] = Response.new(headers, contents, trailers); const futurePromise = future.read().then((result) => result); res._resolve({ tag: "ok", val: null }); @@ -113,15 +114,17 @@ describe("Response", () => { }); }); -describe("Response.body single-stream semantics", () => { - test("throws if body() called twice without closing", async () => { +describe("Response.consumeBody single-stream semantics", () => { + test("throws if consumeBody() called twice without closing", async () => { const headers = new Fields(); const { tx: bodyTx, rx: bodyRx } = stream(); const { tx: trailersTx, rx: trailersRx } = future(); - const { res } = Response.new(headers, bodyRx, trailersRx); + const [res] = Response.new(headers, bodyRx, trailersRx); - res.body(); - expect(() => res.body()).toThrowError(HttpError); + const { rx: resRx2 } = future(); + Response.consumeBody(res, resRx2); + const { rx: resRx3 } = future(); + expect(() => Response.consumeBody(res, resRx3)).toThrowError(HttpError); await bodyTx.close(); await trailersTx.write(null); @@ -131,14 +134,16 @@ describe("Response.body single-stream semantics", () => { const headers = new Fields(); const { tx: bodyTx, rx: bodyRx } = stream(); const { tx: trailersTx, rx: trailersRx } = future(); - const { res } = Response.new(headers, bodyRx, trailersRx); + const [res] = Response.new(headers, bodyRx, trailersRx); - const { body } = res.body(); + const { rx: resRx2 } = future(); + const [body] = Response.consumeBody(res, resRx2); await bodyTx.write(Buffer.from("x")); await bodyTx.close(); await trailersTx.write(null); while ((await body.read()) !== null) {} - expect(() => res.body()).toThrowError(HttpError); + const { rx: resRx3 } = future(); + expect(() => Response.consumeBody(res, resRx3)).toThrowError(HttpError); }); }); diff --git a/packages/preview3-shim/test/http/server.test.js b/packages/preview3-shim/test/http/server.test.js index 5a3ed843e..ea510e0f6 100644 --- a/packages/preview3-shim/test/http/server.test.js +++ b/packages/preview3-shim/test/http/server.test.js @@ -25,7 +25,7 @@ describe("HttpServer Integration", () => { headers.append("content-type", ENCODER.encode("text/plain")); headers.append("trailer", ENCODER.encode("content-md5")); - const { res } = Response.new(headers, bodyRx, trailersRx); + const [res] = Response.new(headers, bodyRx, trailersRx); res.setStatusCode(200); await bodyTx.write(Buffer.from("hello world")); diff --git a/packages/preview3-shim/test/tcp.test.js b/packages/preview3-shim/test/tcp.test.js index cf31f8da6..e8c37def8 100644 --- a/packages/preview3-shim/test/tcp.test.js +++ b/packages/preview3-shim/test/tcp.test.js @@ -156,7 +156,7 @@ describe("TCP Socket Listen", () => { await client.send(rx1); // Receive and verify the message on server - const { stream: srvReader } = conn.receive(); + const [srvReader] = conn.receive(); const buf1 = await srvReader.readAll(); expect(buf1.toString()).toBe(msg1); diff --git a/packages/preview3-shim/types/cli.d.ts b/packages/preview3-shim/types/cli.d.ts index a9f842f92..ce9169444 100644 --- a/packages/preview3-shim/types/cli.d.ts +++ b/packages/preview3-shim/types/cli.d.ts @@ -9,3 +9,4 @@ export type * as terminalOutput from "./interfaces/wasi-cli-terminal-output.d.ts export type * as terminalStderr from "./interfaces/wasi-cli-terminal-stderr.d.ts"; export type * as terminalStdin from "./interfaces/wasi-cli-terminal-stdin.d.ts"; export type * as terminalStdout from "./interfaces/wasi-cli-terminal-stdout.d.ts"; +export type * as types from "./interfaces/wasi-cli-types.d.ts"; diff --git a/packages/preview3-shim/types/clocks.d.ts b/packages/preview3-shim/types/clocks.d.ts index 19cc6c306..7111fd16b 100644 --- a/packages/preview3-shim/types/clocks.d.ts +++ b/packages/preview3-shim/types/clocks.d.ts @@ -1,2 +1,2 @@ export type * as monotonicClock from "./interfaces/wasi-clocks-monotonic-clock.d.ts"; -export type * as wallClock from "./interfaces/wasi-clocks-wall-clock.d.ts"; +export type * as systemClock from "./interfaces/wasi-clocks-system-clock.d.ts"; diff --git a/packages/preview3-shim/types/http.d.ts b/packages/preview3-shim/types/http.d.ts index 55189cac0..67ce12839 100644 --- a/packages/preview3-shim/types/http.d.ts +++ b/packages/preview3-shim/types/http.d.ts @@ -1,2 +1,3 @@ +export type * as client from "./interfaces/wasi-http-client.d.ts"; export type * as handler from "./interfaces/wasi-http-handler.d.ts"; export type * as types from "./interfaces/wasi-http-types.d.ts"; diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-environment.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-environment.d.ts index a481affa8..da5095327 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-environment.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-environment.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:cli/environment@0.3.0 **/ +/** @module Interface wasi:cli/environment@0.3.0-rc-2026-02-09 **/ /** * Get the POSIX-style environment variables. * @@ -18,4 +18,4 @@ export function getArguments(): Array; * Return a path that programs should use as their initial current working * directory, interpreting `.` as shorthand for this. */ -export function initialCwd(): string | undefined; +export function getInitialCwd(): string | undefined; diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-exit.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-exit.d.ts index 3a516cb99..282192fe6 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-exit.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-exit.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:cli/exit@0.3.0 **/ +/** @module Interface wasi:cli/exit@0.3.0-rc-2026-02-09 **/ /** * Exit the current instance and any linked instances. */ diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-run.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-run.d.ts index 7335fb54c..c4c314542 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-run.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-run.d.ts @@ -1,5 +1,5 @@ -/** @module Interface wasi:cli/run@0.3.0 **/ +/** @module Interface wasi:cli/run@0.3.0-rc-2026-02-09 **/ /** * Run the program. */ -export function run(): void; +export function run(): Promise; diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-stderr.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-stderr.d.ts index c9fd99e28..9b265cf11 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-stderr.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-stderr.d.ts @@ -1,2 +1,14 @@ -/** @module Interface wasi:cli/stderr@0.3.0 **/ -export function setStderr(data: ReadableStream): void; +/** @module Interface wasi:cli/stderr@0.3.0-rc-2026-02-09 **/ +/** + * Write the given stream to stderr. + * + * If the stream's writable end is dropped this function will either return + * success once the entire contents of the stream have been written or an + * error-code representing a failure. + * + * Otherwise if there is an error the readable end of the stream will be + * dropped and this function will return an error-code. + */ +export function writeViaStream(data: ReadableStream): Promise>; +export type ErrorCode = import('./wasi-cli-types.js').ErrorCode; +export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-stdin.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-stdin.d.ts index b697343ba..351b2c35a 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-stdin.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-stdin.d.ts @@ -1,2 +1,19 @@ -/** @module Interface wasi:cli/stdin@0.3.0 **/ -export function getStdin(): ReadableStream; +/** @module Interface wasi:cli/stdin@0.3.0-rc-2026-02-09 **/ +/** + * Return a stream for reading from stdin. + * + * This function returns a stream which provides data read from stdin, + * and a future to signal read results. + * + * If the stream's readable end is dropped the future will resolve to success. + * + * If the stream's writable end is dropped the future will either resolve to + * success if stdin was closed by the writer or to an error-code if reading + * failed for some other reason. + * + * Multiple streams may be active at the same time. The behavior of concurrent + * reads is implementation-specific. + */ +export function readViaStream(): [ReadableStream, Promise>]; +export type ErrorCode = import('./wasi-cli-types.js').ErrorCode; +export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-stdout.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-stdout.d.ts index a4d89ef5b..83f4a6038 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-stdout.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-stdout.d.ts @@ -1,2 +1,14 @@ -/** @module Interface wasi:cli/stdout@0.3.0 **/ -export function setStdout(data: ReadableStream): void; +/** @module Interface wasi:cli/stdout@0.3.0-rc-2026-02-09 **/ +/** + * Write the given stream to stdout. + * + * If the stream's writable end is dropped this function will either return + * success once the entire contents of the stream have been written or an + * error-code representing a failure. + * + * Otherwise if there is an error the readable end of the stream will be + * dropped and this function will return an error-code. + */ +export function writeViaStream(data: ReadableStream): Promise>; +export type ErrorCode = import('./wasi-cli-types.js').ErrorCode; +export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-input.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-input.d.ts index 44957389b..e5ba3a517 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-input.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-input.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:cli/terminal-input@0.3.0 **/ +/** @module Interface wasi:cli/terminal-input@0.3.0-rc-2026-02-09 **/ export class TerminalInput { /** diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-output.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-output.d.ts index 7334d0567..0902aef34 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-output.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-output.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:cli/terminal-output@0.3.0 **/ +/** @module Interface wasi:cli/terminal-output@0.3.0-rc-2026-02-09 **/ export class TerminalOutput { /** diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stderr.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stderr.d.ts index 6a3243bf6..096177855 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stderr.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stderr.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:cli/terminal-stderr@0.3.0 **/ +/** @module Interface wasi:cli/terminal-stderr@0.3.0-rc-2026-02-09 **/ /** * If stderr is connected to a terminal, return a `terminal-output` handle * allowing further interaction with it. diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stdin.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stdin.d.ts index 87fceb034..62f18c5ad 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stdin.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stdin.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:cli/terminal-stdin@0.3.0 **/ +/** @module Interface wasi:cli/terminal-stdin@0.3.0-rc-2026-02-09 **/ /** * If stdin is connected to a terminal, return a `terminal-input` handle * allowing further interaction with it. diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stdout.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stdout.d.ts index 895c9193a..7d5ede8a3 100644 --- a/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stdout.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-cli-terminal-stdout.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:cli/terminal-stdout@0.3.0 **/ +/** @module Interface wasi:cli/terminal-stdout@0.3.0-rc-2026-02-09 **/ /** * If stdout is connected to a terminal, return a `terminal-output` handle * allowing further interaction with it. diff --git a/packages/preview3-shim/types/interfaces/wasi-cli-types.d.ts b/packages/preview3-shim/types/interfaces/wasi-cli-types.d.ts new file mode 100644 index 000000000..badf54275 --- /dev/null +++ b/packages/preview3-shim/types/interfaces/wasi-cli-types.d.ts @@ -0,0 +1,15 @@ +/** @module Interface wasi:cli/types@0.3.0-rc-2026-02-09 **/ +/** + * # Variants + * + * ## `"io"` + * + * Input/output error + * ## `"illegal-byte-sequence"` + * + * Invalid or incomplete multibyte or wide character + * ## `"pipe"` + * + * Broken pipe + */ +export type ErrorCode = 'io' | 'illegal-byte-sequence' | 'pipe'; diff --git a/packages/preview3-shim/types/interfaces/wasi-clocks-monotonic-clock.d.ts b/packages/preview3-shim/types/interfaces/wasi-clocks-monotonic-clock.d.ts index 1c1d479d6..d443bbeac 100644 --- a/packages/preview3-shim/types/interfaces/wasi-clocks-monotonic-clock.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-clocks-monotonic-clock.d.ts @@ -1,31 +1,33 @@ -/** @module Interface wasi:clocks/monotonic-clock@0.3.0 **/ +/** @module Interface wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09 **/ /** * Read the current value of the clock. * * The clock is monotonic, therefore calling this function repeatedly will * produce a sequence of non-decreasing values. + * + * For completeness, this function traps if it's not possible to represent + * the value of the clock in a `mark`. Consequently, implementations + * should ensure that the starting time is low enough to avoid the + * possibility of overflow in practice. */ -export function now(): Instant; +export function now(): Mark; /** * Query the resolution of the clock. Returns the duration of time * corresponding to a clock tick. */ -export function resolution(): Duration; +export function getResolution(): Duration; /** - * Wait until the specified instant has occurred. + * Wait until the specified mark has occurred. */ -export function waitUntil(when: Instant): Promise; +export function waitUntil(when: Mark): Promise; /** - * Wait for the specified duration has elapsed. + * Wait for the specified duration to elapse. */ export function waitFor(howLong: Duration): Promise; +export type Duration = import('./wasi-clocks-types.js').Duration; /** - * An instant in time, in nanoseconds. An instant is relative to an + * A mark on a monotonic clock is a number of nanoseconds since an * unspecified initial value, and can only be compared to instances from * the same monotonic-clock. */ -export type Instant = bigint; -/** - * A duration of time, in nanoseconds. - */ -export type Duration = bigint; +export type Mark = bigint; diff --git a/packages/preview3-shim/types/interfaces/wasi-clocks-system-clock.d.ts b/packages/preview3-shim/types/interfaces/wasi-clocks-system-clock.d.ts new file mode 100644 index 000000000..3b8b1b2d1 --- /dev/null +++ b/packages/preview3-shim/types/interfaces/wasi-clocks-system-clock.d.ts @@ -0,0 +1,38 @@ +/** @module Interface wasi:clocks/system-clock@0.3.0-rc-2026-02-09 **/ +/** + * 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 nanoseconds field of the output is always less than 1000000000. + */ +export function now(): Instant; +/** + * Query the resolution of the clock. Returns the smallest duration of time + * that the implementation permits distinguishing. + */ +export function getResolution(): Duration; +export type Duration = import('./wasi-clocks-types.js').Duration; +/** + * An "instant", or "exact time", is a point in time without regard to any + * time zone: just the time since a particular external reference point, + * often called an "epoch". + * + * Here, the epoch is 1970-01-01T00:00:00Z, also known as + * [POSIX's Seconds Since the Epoch], also known as [Unix Time]. + * + * Note that even if the seconds field is negative, incrementing + * nanoseconds always represents moving forwards in time. + * For example, `{ -1 seconds, 999999999 nanoseconds }` represents the + * instant one nanosecond before the epoch. + * For more on various different ways to represent time, see + * https://tc39.es/proposal-temporal/docs/timezone.html + * + * [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 + */ +export interface Instant { + seconds: bigint, + nanoseconds: number, +} diff --git a/packages/preview3-shim/types/interfaces/wasi-clocks-types.d.ts b/packages/preview3-shim/types/interfaces/wasi-clocks-types.d.ts new file mode 100644 index 000000000..ebf39219a --- /dev/null +++ b/packages/preview3-shim/types/interfaces/wasi-clocks-types.d.ts @@ -0,0 +1,5 @@ +/** @module Interface wasi:clocks/types@0.3.0-rc-2026-02-09 **/ +/** + * A duration of time, in nanoseconds. + */ +export type Duration = bigint; diff --git a/packages/preview3-shim/types/interfaces/wasi-clocks-wall-clock.d.ts b/packages/preview3-shim/types/interfaces/wasi-clocks-wall-clock.d.ts deleted file mode 100644 index efd21513b..000000000 --- a/packages/preview3-shim/types/interfaces/wasi-clocks-wall-clock.d.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** @module Interface wasi:clocks/wall-clock@0.3.0 **/ -/** - * 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 - */ -export function now(): Datetime; -/** - * Query the resolution of the clock. - * - * The nanoseconds field of the output is always less than 1000000000. - */ -export function resolution(): Datetime; -/** - * A time and date in seconds plus nanoseconds. - */ -export interface Datetime { - seconds: bigint, - nanoseconds: number, -} diff --git a/packages/preview3-shim/types/interfaces/wasi-filesystem-preopens.d.ts b/packages/preview3-shim/types/interfaces/wasi-filesystem-preopens.d.ts index 1cd3dc0c6..4c7ac034b 100644 --- a/packages/preview3-shim/types/interfaces/wasi-filesystem-preopens.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-filesystem-preopens.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:filesystem/preopens@0.3.0 **/ +/** @module Interface wasi:filesystem/preopens@0.3.0-rc-2026-02-09 **/ /** * Return the set of preopened directories, and their paths. */ diff --git a/packages/preview3-shim/types/interfaces/wasi-filesystem-types.d.ts b/packages/preview3-shim/types/interfaces/wasi-filesystem-types.d.ts index 0ac1f5d81..6d4f7441b 100644 --- a/packages/preview3-shim/types/interfaces/wasi-filesystem-types.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-filesystem-types.d.ts @@ -1,5 +1,5 @@ -/** @module Interface wasi:filesystem/types@0.3.0 **/ -export type Datetime = import('./wasi-clocks-wall-clock.js').Datetime; +/** @module Interface wasi:filesystem/types@0.3.0-rc-2026-02-09 **/ +export type Instant = import('./wasi-clocks-system-clock.js').Instant; /** * File size or length of a region within a file. */ @@ -153,21 +153,21 @@ export interface DescriptorStat { * If the `option` is none, the platform doesn't maintain an access * timestamp for this file. */ - dataAccessTimestamp?: Datetime, + dataAccessTimestamp?: Instant, /** * Last data modification timestamp. * * If the `option` is none, the platform doesn't maintain a * modification timestamp for this file. */ - dataModificationTimestamp?: Datetime, + dataModificationTimestamp?: Instant, /** * Last file status-change timestamp. * * If the `option` is none, the platform doesn't maintain a * status-change timestamp for this file. */ - statusChangeTimestamp?: Datetime, + statusChangeTimestamp?: Instant, } /** * When setting a timestamp, this gives the value to set it to. @@ -191,7 +191,7 @@ export interface NewTimestampNow { */ export interface NewTimestampTimestamp { tag: 'timestamp', - val: Datetime, + val: Instant, } /** * A directory entry. @@ -380,8 +380,15 @@ export class Descriptor { * Multiple read, write, and append streams may be active on the same open * file and they do not interfere with each other. * - * This function returns a future, which will resolve to an error code if - * reading full contents of the file fails. + * This function returns a `stream` which provides the data received from the + * file, and a `future` providing additional error information in case an + * error is encountered. + * + * If no error is encountered, `stream.read` on the `stream` will return + * `read-status::closed` with no `error-context` and the future resolves to + * the value `ok`. If an error is encountered, `stream.read` on the + * `stream` returns `read-status::closed` with an `error-context` and the future + * resolves to `err` with an `error-code`. * * Note: This is similar to `pread` in POSIX. */ @@ -400,7 +407,7 @@ export class Descriptor { * * Note: This is similar to `pwrite` in POSIX. */ - writeViaStream(data: ReadableStream, offset: Filesize): Promise; + writeViaStream(data: ReadableStream, offset: Filesize): Promise>; /** * Return a stream for appending to a file, if available. * @@ -411,7 +418,7 @@ export class Descriptor { * * Note: This is similar to `write` with `O_APPEND` in POSIX. */ - appendViaStream(data: ReadableStream): Promise; + appendViaStream(data: ReadableStream): Promise>; /** * Provide file advisory information on a descriptor. * @@ -478,7 +485,7 @@ export class Descriptor { * This function returns a future, which will resolve to an error code if * reading full contents of the directory fails. */ - readDirectory(): Promise<[ReadableStream, Promise>]>; + readDirectory(): [ReadableStream, Promise>]; /** * Synchronize the data and metadata of a file to disk. * diff --git a/packages/preview3-shim/types/interfaces/wasi-http-client.d.ts b/packages/preview3-shim/types/interfaces/wasi-http-client.d.ts new file mode 100644 index 000000000..17b334ca3 --- /dev/null +++ b/packages/preview3-shim/types/interfaces/wasi-http-client.d.ts @@ -0,0 +1,9 @@ +/** @module Interface wasi:http/client@0.3.0-rc-2026-02-09 **/ +/** + * This function may be used to either send an outgoing request over the + * network or to forward it to another component. + */ +export function send(request: Request): Promise; +export type Request = import('./wasi-http-types.js').Request; +export type Response = import('./wasi-http-types.js').Response; +export type ErrorCode = import('./wasi-http-types.js').ErrorCode; diff --git a/packages/preview3-shim/types/interfaces/wasi-http-handler.d.ts b/packages/preview3-shim/types/interfaces/wasi-http-handler.d.ts index c2b87ad72..6a34503cd 100644 --- a/packages/preview3-shim/types/interfaces/wasi-http-handler.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-http-handler.d.ts @@ -1,11 +1,7 @@ -/** @module Interface wasi:http/handler@0.3.0-draft **/ +/** @module Interface wasi:http/handler@0.3.0-rc-2026-02-09 **/ /** - * When exported, this function may be called with either an incoming - * request read from the network or a request synthesized or forwarded by - * another component. - * - * When imported, this function may be used to either send an outgoing - * request over the network or pass it to another component. + * This function may be called with either an incoming request read from the + * network or a request synthesized or forwarded by another component. */ export function handle(request: Request): Promise; export type Request = import('./wasi-http-types.js').Request; diff --git a/packages/preview3-shim/types/interfaces/wasi-http-types.d.ts b/packages/preview3-shim/types/interfaces/wasi-http-types.d.ts index 22782d5e7..2b26cbf9b 100644 --- a/packages/preview3-shim/types/interfaces/wasi-http-types.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-http-types.d.ts @@ -1,5 +1,5 @@ -/** @module Interface wasi:http/types@0.3.0-draft **/ -export type Duration = import('./wasi-clocks-monotonic-clock.js').Duration; +/** @module Interface wasi:http/types@0.3.0-rc-2026-02-09 **/ +export type Duration = import('./wasi-clocks-types.js').Duration; /** * This type corresponds to HTTP standard Methods. */ @@ -364,10 +364,10 @@ export class Fields { * The names and values are always returned in the original casing and in * the order in which they will be serialized for transport. */ - entries(): Array<[FieldName, FieldValue]>; + copyAll(): Array<[FieldName, FieldValue]>; /** * Make a deep copy of the Fields. Equivalent in behavior to calling the - * `fields` constructor on the return value of `entries`. The resulting + * `fields` constructor on the return value of `copy-all`. The resulting * `fields` is mutable. */ clone(): Fields; @@ -384,7 +384,8 @@ export class Request { * * `headers` is the HTTP Headers for the Request. * - * `contents` is the optional body content stream. + * `contents` is the optional body content stream with `none` + * representing a zero-length content stream. * Once it is closed, `trailers` future must resolve to a result. * If `trailers` resolves to an error, underlying connection * will be closed immediately. @@ -404,7 +405,7 @@ export class Request { /** * Get the Method for the Request. */ - method(): Method; + getMethod(): Method; /** * Set the Method for the Request. Fails if the string present in a * `method.other` argument is not a syntactically valid method. @@ -414,7 +415,7 @@ export class Request { * Get the combination of the HTTP Path and Query for the Request. When * `none`, this represents an empty Path and empty Query. */ - pathWithQuery(): string | undefined; + getPathWithQuery(): string | undefined; /** * 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 @@ -425,7 +426,7 @@ export class Request { * Get the HTTP Related Scheme for the Request. When `none`, the * implementation may choose an appropriate default scheme. */ - scheme(): Scheme | undefined; + getScheme(): Scheme | undefined; /** * Set the HTTP Related Scheme for the Request. When `none`, the * implementation may choose an appropriate default scheme. Fails if the @@ -437,7 +438,7 @@ export class Request { * with Related Schemes which do not require an authority. The HTTP and * HTTPS schemes always require an authority. */ - authority(): string | undefined; + getAuthority(): string | undefined; /** * 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 @@ -455,36 +456,29 @@ export class Request { * the parent `request` is dropped, or its ownership is transferred to * another component by e.g. `handler.handle`. */ - options(): RequestOptions | undefined; + getOptions(): RequestOptions | undefined; /** * Get the headers associated with the Request. * * The returned `headers` resource is immutable: `set`, `append`, and * `delete` operations will fail with `header-error.immutable`. */ - headers(): Headers; + getHeaders(): Headers; /** * Get body of the Request. * * Stream returned by this method represents the contents of the body. - * Once the stream is reported as closed, callers should await the returned future - * to determine whether the body was received successfully. + * Once the stream is reported as closed, callers should await the returned + * future to determine whether the body was received successfully. * The future will only resolve after the stream is reported as closed. * - * The stream and future returned by this method are children: - * they should be closed or consumed before the parent `response` - * is dropped, or its ownership is transferred to another component - * by e.g. `handler.handle`. + * This function takes a `res` future as a parameter, which can be used to + * communicate an error in handling of the request. * - * This method may be called multiple times. - * - * This method will return an error if it is called while either: - * - a stream or future returned by a previous call to this method is still open - * - a stream returned by a previous call to this method has reported itself as closed - * Thus there will always be at most one readable stream open for a given body. - * Each subsequent stream picks up where the last stream left off, up until it is finished. + * Note that function will move the `request`, but references to headers or + * request options acquired from it previously will remain valid. */ - body(): [ReadableStream, Promise>]; + static consumeBody(this_: Request, res: Promise>): [ReadableStream, Promise>]; } export class RequestOptions { @@ -495,7 +489,7 @@ export class RequestOptions { /** * The timeout for the initial connect to the HTTP Server. */ - connectTimeout(): Duration | undefined; + getConnectTimeout(): Duration | undefined; /** * Set the timeout for the initial connect to the HTTP Server. An error * return value indicates that this timeout is not supported or that this @@ -505,7 +499,7 @@ export class RequestOptions { /** * The timeout for receiving the first byte of the Response body. */ - firstByteTimeout(): Duration | undefined; + getFirstByteTimeout(): Duration | undefined; /** * Set the timeout for receiving the first byte of the Response body. An * error return value indicates that this timeout is not supported or that @@ -516,7 +510,7 @@ export class RequestOptions { * The timeout for receiving subsequent chunks of bytes in the Response * body stream. */ - betweenBytesTimeout(): Duration | undefined; + getBetweenBytesTimeout(): Duration | undefined; /** * Set the timeout for receiving subsequent chunks of bytes in the Response * body stream. An error return value indicates that this timeout is not @@ -542,7 +536,8 @@ export class Response { * * `headers` is the HTTP Headers for the Response. * - * `contents` is the optional body content stream. + * `contents` is the optional body content stream with `none` + * representing a zero-length content stream. * Once it is closed, `trailers` future must resolve to a result. * If `trailers` resolves to an error, underlying connection * will be closed immediately. @@ -553,7 +548,7 @@ export class Response { /** * Get the HTTP Status Code for the Response. */ - statusCode(): StatusCode; + getStatusCode(): StatusCode; /** * Set the HTTP Status Code for the Response. Fails if the status-code * given is not a valid http status code. @@ -565,27 +560,20 @@ export class Response { * The returned `headers` resource is immutable: `set`, `append`, and * `delete` operations will fail with `header-error.immutable`. */ - headers(): Headers; + getHeaders(): Headers; /** * Get body of the Response. * * Stream returned by this method represents the contents of the body. - * Once the stream is reported as closed, callers should await the returned future - * to determine whether the body was received successfully. + * Once the stream is reported as closed, callers should await the returned + * future to determine whether the body was received successfully. * The future will only resolve after the stream is reported as closed. * - * The stream and future returned by this method are children: - * they should be closed or consumed before the parent `response` - * is dropped, or its ownership is transferred to another component - * by e.g. `handler.handle`. - * - * This method may be called multiple times. + * This function takes a `res` future as a parameter, which can be used to + * communicate an error in handling of the response. * - * This method will return an error if it is called while either: - * - a stream or future returned by a previous call to this method is still open - * - a stream returned by a previous call to this method has reported itself as closed - * Thus there will always be at most one readable stream open for a given body. - * Each subsequent stream picks up where the last stream left off, up until it is finished. + * Note that function will move the `response`, but references to headers + * acquired from it previously will remain valid. */ - body(): [ReadableStream, Promise>]; + static consumeBody(this_: Response, res: Promise>): [ReadableStream, Promise>]; } diff --git a/packages/preview3-shim/types/interfaces/wasi-random-insecure-seed.d.ts b/packages/preview3-shim/types/interfaces/wasi-random-insecure-seed.d.ts index 3e2e28fb6..f087b5c54 100644 --- a/packages/preview3-shim/types/interfaces/wasi-random-insecure-seed.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-random-insecure-seed.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:random/insecure-seed@0.3.0 **/ +/** @module Interface wasi:random/insecure-seed@0.3.0-rc-2026-02-09 **/ /** * Return a 128-bit value that may contain a pseudo-random value. * @@ -18,4 +18,4 @@ * called multiple times and potentially used for purposes other than DoS * protection. */ -export function insecureSeed(): [bigint, bigint]; +export function getInsecureSeed(): [bigint, bigint]; diff --git a/packages/preview3-shim/types/interfaces/wasi-random-insecure.d.ts b/packages/preview3-shim/types/interfaces/wasi-random-insecure.d.ts index ddc524e00..02a2310e2 100644 --- a/packages/preview3-shim/types/interfaces/wasi-random-insecure.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-random-insecure.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:random/insecure@0.3.0 **/ +/** @module Interface wasi:random/insecure@0.3.0-rc-2026-02-09 **/ /** * Return `len` insecure pseudo-random bytes. * diff --git a/packages/preview3-shim/types/interfaces/wasi-random-random.d.ts b/packages/preview3-shim/types/interfaces/wasi-random-random.d.ts index b33e1f55c..5ab0747f1 100644 --- a/packages/preview3-shim/types/interfaces/wasi-random-random.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-random-random.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:random/random@0.3.0 **/ +/** @module Interface wasi:random/random@0.3.0-rc-2026-02-09 **/ /** * Return `len` cryptographically-secure random or pseudo-random bytes. * diff --git a/packages/preview3-shim/types/interfaces/wasi-sockets-ip-name-lookup.d.ts b/packages/preview3-shim/types/interfaces/wasi-sockets-ip-name-lookup.d.ts index c50618fa0..0008c05ae 100644 --- a/packages/preview3-shim/types/interfaces/wasi-sockets-ip-name-lookup.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-sockets-ip-name-lookup.d.ts @@ -1,4 +1,4 @@ -/** @module Interface wasi:sockets/ip-name-lookup@0.3.0 **/ +/** @module Interface wasi:sockets/ip-name-lookup@0.3.0-rc-2026-02-09 **/ /** * Resolve an internet host name to a list of IP addresses. * diff --git a/packages/preview3-shim/types/interfaces/wasi-sockets-types.d.ts b/packages/preview3-shim/types/interfaces/wasi-sockets-types.d.ts index fd91223df..a4cc8b7a0 100644 --- a/packages/preview3-shim/types/interfaces/wasi-sockets-types.d.ts +++ b/packages/preview3-shim/types/interfaces/wasi-sockets-types.d.ts @@ -1,5 +1,5 @@ -/** @module Interface wasi:sockets/types@0.3.0 **/ -export type Duration = import('./wasi-clocks-monotonic-clock.js').Duration; +/** @module Interface wasi:sockets/types@0.3.0-rc-2026-02-09 **/ +export type Duration = import('./wasi-clocks-types.js').Duration; /** * Error codes. * @@ -129,6 +129,10 @@ export interface IpSocketAddressIpv6 { export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; export class TcpSocket { + /** + * This type does not have a public constructor. + */ + private constructor(); /** * Create a new TCP socket. * @@ -145,7 +149,7 @@ export class TcpSocket { * - * - */ - constructor(addressFamily: IpAddressFamily) + static create(addressFamily: IpAddressFamily): TcpSocket; /** * Bind the socket to the provided IP address and port. * @@ -213,7 +217,7 @@ export class TcpSocket { */ connect(remoteAddress: IpSocketAddress): Promise; /** - * Start listening return a stream of new inbound connections. + * Start listening and return a stream of new inbound connections. * * Transitions the socket into the `listening` state. This can be called * at most once per socket. @@ -268,6 +272,12 @@ export class TcpSocket { * In either case, the stream returned by this `listen` method remains * operational. * + * WASI requires `listen` to perform an implicit bind if the socket + * has not already been bound. Not all platforms (notably Windows) + * exhibit this behavior out of the box. On platforms that require it, + * the WASI implementation can emulate this behavior by performing + * the bind itself if the guest hasn't already done so. + * * # References * - * - @@ -301,7 +311,7 @@ export class TcpSocket { * - * - */ - send(data: ReadableStream): Promise; + send(data: ReadableStream): Promise>; /** * Read data from peer. * @@ -343,7 +353,7 @@ export class TcpSocket { * > 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. + * WASI is stricter and requires `get-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. @@ -354,7 +364,7 @@ export class TcpSocket { * - * - */ - localAddress(): IpSocketAddress; + getLocalAddress(): IpSocketAddress; /** * Get the remote address. * @@ -367,13 +377,13 @@ export class TcpSocket { * - * - */ - remoteAddress(): IpSocketAddress; + getRemoteAddress(): IpSocketAddress; /** * Whether the socket is in the `listening` state. * * Equivalent to the SO_ACCEPTCONN socket option. */ - isListening(): boolean; + getIsListening(): boolean; /** * Whether this is a IPv4 or IPv6 socket. * @@ -381,7 +391,7 @@ export class TcpSocket { * * Equivalent to the SO_DOMAIN socket option. */ - addressFamily(): IpAddressFamily; + getAddressFamily(): IpAddressFamily; /** * Hints the desired listen queue size. Implementations are free to ignore this. * @@ -405,7 +415,7 @@ export class TcpSocket { * * Equivalent to the SO_KEEPALIVE socket option. */ - keepAliveEnabled(): boolean; + getKeepAliveEnabled(): boolean; setKeepAliveEnabled(value: boolean): void; /** * Amount of time the connection has to be idle before TCP starts sending keepalive packets. @@ -419,7 +429,7 @@ export class TcpSocket { * # Typical errors * - `invalid-argument`: (set) The provided value was 0. */ - keepAliveIdleTime(): Duration; + getKeepAliveIdleTime(): Duration; setKeepAliveIdleTime(value: Duration): void; /** * The time between keepalive packets. @@ -433,7 +443,7 @@ export class TcpSocket { * # Typical errors * - `invalid-argument`: (set) The provided value was 0. */ - keepAliveInterval(): Duration; + getKeepAliveInterval(): Duration; setKeepAliveInterval(value: Duration): void; /** * The maximum amount of keepalive packets TCP should send before aborting the connection. @@ -447,7 +457,7 @@ export class TcpSocket { * # Typical errors * - `invalid-argument`: (set) The provided value was 0. */ - keepAliveCount(): number; + getKeepAliveCount(): number; setKeepAliveCount(value: number): void; /** * Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. @@ -457,7 +467,7 @@ export class TcpSocket { * # Typical errors * - `invalid-argument`: (set) The TTL value must be 1 or higher. */ - hopLimit(): number; + getHopLimit(): number; setHopLimit(value: number): void; /** * The kernel buffer space reserved for sends/receives on this socket. @@ -471,13 +481,17 @@ export class TcpSocket { * # Typical errors * - `invalid-argument`: (set) The provided value was 0. */ - receiveBufferSize(): bigint; + getReceiveBufferSize(): bigint; setReceiveBufferSize(value: bigint): void; - sendBufferSize(): bigint; + getSendBufferSize(): bigint; setSendBufferSize(value: bigint): void; } export class UdpSocket { + /** + * This type does not have a public constructor. + */ + private constructor(); /** * Create a new UDP socket. * @@ -494,7 +508,7 @@ export class UdpSocket { * - * - */ - constructor(addressFamily: IpAddressFamily) + static create(addressFamily: IpAddressFamily): UdpSocket; /** * Bind the socket to the provided IP address and port. * @@ -640,7 +654,7 @@ export class UdpSocket { * > 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. + * WASI is stricter and requires `get-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. @@ -651,7 +665,7 @@ export class UdpSocket { * - * - */ - localAddress(): IpSocketAddress; + getLocalAddress(): IpSocketAddress; /** * Get the address the socket is currently "connected" to. * @@ -664,7 +678,7 @@ export class UdpSocket { * - * - */ - remoteAddress(): IpSocketAddress; + getRemoteAddress(): IpSocketAddress; /** * Whether this is a IPv4 or IPv6 socket. * @@ -672,7 +686,7 @@ export class UdpSocket { * * Equivalent to the SO_DOMAIN socket option. */ - addressFamily(): IpAddressFamily; + getAddressFamily(): IpAddressFamily; /** * Equivalent to the IP_TTL & IPV6_UNICAST_HOPS socket options. * @@ -681,7 +695,7 @@ export class UdpSocket { * # Typical errors * - `invalid-argument`: (set) The TTL value must be 1 or higher. */ - unicastHopLimit(): number; + getUnicastHopLimit(): number; setUnicastHopLimit(value: number): void; /** * The kernel buffer space reserved for sends/receives on this socket. @@ -695,8 +709,8 @@ export class UdpSocket { * # Typical errors * - `invalid-argument`: (set) The provided value was 0. */ - receiveBufferSize(): bigint; + getReceiveBufferSize(): bigint; setReceiveBufferSize(value: bigint): void; - sendBufferSize(): bigint; + getSendBufferSize(): bigint; setSendBufferSize(value: bigint): void; } diff --git a/packages/preview3-shim/types/wasi-cli-command.d.ts b/packages/preview3-shim/types/wasi-cli-command.d.ts index 55529ccdb..6dc3ae7c3 100644 --- a/packages/preview3-shim/types/wasi-cli-command.d.ts +++ b/packages/preview3-shim/types/wasi-cli-command.d.ts @@ -1,21 +1,23 @@ -// world wasi:cli/command@0.3.0 -export type * as WasiCliEnvironment030 from './interfaces/wasi-cli-environment.js'; // import wasi:cli/environment@0.3.0 -export type * as WasiCliExit030 from './interfaces/wasi-cli-exit.js'; // import wasi:cli/exit@0.3.0 -export type * as WasiCliStderr030 from './interfaces/wasi-cli-stderr.js'; // import wasi:cli/stderr@0.3.0 -export type * as WasiCliStdin030 from './interfaces/wasi-cli-stdin.js'; // import wasi:cli/stdin@0.3.0 -export type * as WasiCliStdout030 from './interfaces/wasi-cli-stdout.js'; // import wasi:cli/stdout@0.3.0 -export type * as WasiCliTerminalInput030 from './interfaces/wasi-cli-terminal-input.js'; // import wasi:cli/terminal-input@0.3.0 -export type * as WasiCliTerminalOutput030 from './interfaces/wasi-cli-terminal-output.js'; // import wasi:cli/terminal-output@0.3.0 -export type * as WasiCliTerminalStderr030 from './interfaces/wasi-cli-terminal-stderr.js'; // import wasi:cli/terminal-stderr@0.3.0 -export type * as WasiCliTerminalStdin030 from './interfaces/wasi-cli-terminal-stdin.js'; // import wasi:cli/terminal-stdin@0.3.0 -export type * as WasiCliTerminalStdout030 from './interfaces/wasi-cli-terminal-stdout.js'; // import wasi:cli/terminal-stdout@0.3.0 -export type * as WasiClocksMonotonicClock030 from './interfaces/wasi-clocks-monotonic-clock.js'; // import wasi:clocks/monotonic-clock@0.3.0 -export type * as WasiClocksWallClock030 from './interfaces/wasi-clocks-wall-clock.js'; // import wasi:clocks/wall-clock@0.3.0 -export type * as WasiFilesystemPreopens030 from './interfaces/wasi-filesystem-preopens.js'; // import wasi:filesystem/preopens@0.3.0 -export type * as WasiFilesystemTypes030 from './interfaces/wasi-filesystem-types.js'; // import wasi:filesystem/types@0.3.0 -export type * as WasiRandomInsecureSeed030 from './interfaces/wasi-random-insecure-seed.js'; // import wasi:random/insecure-seed@0.3.0 -export type * as WasiRandomInsecure030 from './interfaces/wasi-random-insecure.js'; // import wasi:random/insecure@0.3.0 -export type * as WasiRandomRandom030 from './interfaces/wasi-random-random.js'; // import wasi:random/random@0.3.0 -export type * as WasiSocketsIpNameLookup030 from './interfaces/wasi-sockets-ip-name-lookup.js'; // import wasi:sockets/ip-name-lookup@0.3.0 -export type * as WasiSocketsTypes030 from './interfaces/wasi-sockets-types.js'; // import wasi:sockets/types@0.3.0 -export * as run from './interfaces/wasi-cli-run.js'; // export wasi:cli/run@0.3.0 +// world wasi:cli/command@0.3.0-rc-2026-02-09 +export type * as WasiCliEnvironment030Rc20260209 from './interfaces/wasi-cli-environment.js'; // import wasi:cli/environment@0.3.0-rc-2026-02-09 +export type * as WasiCliExit030Rc20260209 from './interfaces/wasi-cli-exit.js'; // import wasi:cli/exit@0.3.0-rc-2026-02-09 +export type * as WasiCliStderr030Rc20260209 from './interfaces/wasi-cli-stderr.js'; // import wasi:cli/stderr@0.3.0-rc-2026-02-09 +export type * as WasiCliStdin030Rc20260209 from './interfaces/wasi-cli-stdin.js'; // import wasi:cli/stdin@0.3.0-rc-2026-02-09 +export type * as WasiCliStdout030Rc20260209 from './interfaces/wasi-cli-stdout.js'; // import wasi:cli/stdout@0.3.0-rc-2026-02-09 +export type * as WasiCliTerminalInput030Rc20260209 from './interfaces/wasi-cli-terminal-input.js'; // import wasi:cli/terminal-input@0.3.0-rc-2026-02-09 +export type * as WasiCliTerminalOutput030Rc20260209 from './interfaces/wasi-cli-terminal-output.js'; // import wasi:cli/terminal-output@0.3.0-rc-2026-02-09 +export type * as WasiCliTerminalStderr030Rc20260209 from './interfaces/wasi-cli-terminal-stderr.js'; // import wasi:cli/terminal-stderr@0.3.0-rc-2026-02-09 +export type * as WasiCliTerminalStdin030Rc20260209 from './interfaces/wasi-cli-terminal-stdin.js'; // import wasi:cli/terminal-stdin@0.3.0-rc-2026-02-09 +export type * as WasiCliTerminalStdout030Rc20260209 from './interfaces/wasi-cli-terminal-stdout.js'; // import wasi:cli/terminal-stdout@0.3.0-rc-2026-02-09 +export type * as WasiCliTypes030Rc20260209 from './interfaces/wasi-cli-types.js'; // import wasi:cli/types@0.3.0-rc-2026-02-09 +export type * as WasiClocksMonotonicClock030Rc20260209 from './interfaces/wasi-clocks-monotonic-clock.js'; // import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09 +export type * as WasiClocksSystemClock030Rc20260209 from './interfaces/wasi-clocks-system-clock.js'; // import wasi:clocks/system-clock@0.3.0-rc-2026-02-09 +export type * as WasiClocksTypes030Rc20260209 from './interfaces/wasi-clocks-types.js'; // import wasi:clocks/types@0.3.0-rc-2026-02-09 +export type * as WasiFilesystemPreopens030Rc20260209 from './interfaces/wasi-filesystem-preopens.js'; // import wasi:filesystem/preopens@0.3.0-rc-2026-02-09 +export type * as WasiFilesystemTypes030Rc20260209 from './interfaces/wasi-filesystem-types.js'; // import wasi:filesystem/types@0.3.0-rc-2026-02-09 +export type * as WasiRandomInsecureSeed030Rc20260209 from './interfaces/wasi-random-insecure-seed.js'; // import wasi:random/insecure-seed@0.3.0-rc-2026-02-09 +export type * as WasiRandomInsecure030Rc20260209 from './interfaces/wasi-random-insecure.js'; // import wasi:random/insecure@0.3.0-rc-2026-02-09 +export type * as WasiRandomRandom030Rc20260209 from './interfaces/wasi-random-random.js'; // import wasi:random/random@0.3.0-rc-2026-02-09 +export type * as WasiSocketsIpNameLookup030Rc20260209 from './interfaces/wasi-sockets-ip-name-lookup.js'; // import wasi:sockets/ip-name-lookup@0.3.0-rc-2026-02-09 +export type * as WasiSocketsTypes030Rc20260209 from './interfaces/wasi-sockets-types.js'; // import wasi:sockets/types@0.3.0-rc-2026-02-09 +export * as run from './interfaces/wasi-cli-run.js'; // export wasi:cli/run@0.3.0-rc-2026-02-09 diff --git a/packages/preview3-shim/types/wasi-http-proxy.d.ts b/packages/preview3-shim/types/wasi-http-proxy.d.ts deleted file mode 100644 index d6dda27ab..000000000 --- a/packages/preview3-shim/types/wasi-http-proxy.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -// world wasi:http/proxy@0.3.0-draft -export type * as WasiCliStderr030 from './interfaces/wasi-cli-stderr.js'; // import wasi:cli/stderr@0.3.0 -export type * as WasiCliStdin030 from './interfaces/wasi-cli-stdin.js'; // import wasi:cli/stdin@0.3.0 -export type * as WasiCliStdout030 from './interfaces/wasi-cli-stdout.js'; // import wasi:cli/stdout@0.3.0 -export type * as WasiClocksMonotonicClock030 from './interfaces/wasi-clocks-monotonic-clock.js'; // import wasi:clocks/monotonic-clock@0.3.0 -export type * as WasiClocksWallClock030 from './interfaces/wasi-clocks-wall-clock.js'; // import wasi:clocks/wall-clock@0.3.0 -export type * as WasiHttpHandler030Draft from './interfaces/wasi-http-handler.js'; // import wasi:http/handler@0.3.0-draft -export type * as WasiHttpTypes030Draft from './interfaces/wasi-http-types.js'; // import wasi:http/types@0.3.0-draft -export type * as WasiRandomRandom030 from './interfaces/wasi-random-random.js'; // import wasi:random/random@0.3.0 -export * as handler from './interfaces/wasi-http-handler.js'; // export wasi:http/handler@0.3.0-draft diff --git a/packages/preview3-shim/types/wasi-http-service.d.ts b/packages/preview3-shim/types/wasi-http-service.d.ts new file mode 100644 index 000000000..fbfece75a --- /dev/null +++ b/packages/preview3-shim/types/wasi-http-service.d.ts @@ -0,0 +1,14 @@ +// world wasi:http/service@0.3.0-rc-2026-02-09 +export type * as WasiCliStderr030Rc20260209 from './interfaces/wasi-cli-stderr.js'; // import wasi:cli/stderr@0.3.0-rc-2026-02-09 +export type * as WasiCliStdin030Rc20260209 from './interfaces/wasi-cli-stdin.js'; // import wasi:cli/stdin@0.3.0-rc-2026-02-09 +export type * as WasiCliStdout030Rc20260209 from './interfaces/wasi-cli-stdout.js'; // import wasi:cli/stdout@0.3.0-rc-2026-02-09 +export type * as WasiCliTypes030Rc20260209 from './interfaces/wasi-cli-types.js'; // import wasi:cli/types@0.3.0-rc-2026-02-09 +export type * as WasiClocksMonotonicClock030Rc20260209 from './interfaces/wasi-clocks-monotonic-clock.js'; // import wasi:clocks/monotonic-clock@0.3.0-rc-2026-02-09 +export type * as WasiClocksSystemClock030Rc20260209 from './interfaces/wasi-clocks-system-clock.js'; // import wasi:clocks/system-clock@0.3.0-rc-2026-02-09 +export type * as WasiClocksTypes030Rc20260209 from './interfaces/wasi-clocks-types.js'; // import wasi:clocks/types@0.3.0-rc-2026-02-09 +export type * as WasiHttpClient030Rc20260209 from './interfaces/wasi-http-client.js'; // import wasi:http/client@0.3.0-rc-2026-02-09 +export type * as WasiHttpTypes030Rc20260209 from './interfaces/wasi-http-types.js'; // import wasi:http/types@0.3.0-rc-2026-02-09 +export type * as WasiRandomInsecureSeed030Rc20260209 from './interfaces/wasi-random-insecure-seed.js'; // import wasi:random/insecure-seed@0.3.0-rc-2026-02-09 +export type * as WasiRandomInsecure030Rc20260209 from './interfaces/wasi-random-insecure.js'; // import wasi:random/insecure@0.3.0-rc-2026-02-09 +export type * as WasiRandomRandom030Rc20260209 from './interfaces/wasi-random-random.js'; // import wasi:random/random@0.3.0-rc-2026-02-09 +export * as handler from './interfaces/wasi-http-handler.js'; // export wasi:http/handler@0.3.0-rc-2026-02-09 diff --git a/scripts/update-wasi-p3.sh b/scripts/update-wasi-p3.sh new file mode 100755 index 000000000..bcae0f08b --- /dev/null +++ b/scripts/update-wasi-p3.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +# Update WASI Preview 3 WIT dependencies and regenerate TypeScript types. +# Requires wkg (https://github.com/bytecodealliance/wasm-pkg-tools) +set -ex + +REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" +WIT_DIR="$REPO_ROOT/packages/jco/test/fixtures/p3/wit" +TYPES_DIR="$REPO_ROOT/packages/preview3-shim/types" + +# Update world.wit to the latest version from the registry and we extract it from there +VERSION=$(grep -oP '@\K[0-9a-z.\-]+' "$WIT_DIR/world.wit" | head -1) +if [ -z "$VERSION" ]; then + echo "Error: could not extract version from world.wit" >&2 + exit 1 +fi + +echo "Fetching WASI packages at version $VERSION" + +PACKAGES=(clocks cli filesystem http random sockets) + +rm -rf "$WIT_DIR/deps" +for pkg in "${PACKAGES[@]}"; do + dir="$WIT_DIR/deps/wasi-${pkg}-${VERSION}" + mkdir -p "$dir" + wkg get "wasi:${pkg}@${VERSION}" --format wit -o "$dir/package.wit" --overwrite +done + +cd "$REPO_ROOT" +cargo xtask generate wasi-types preview3 + +echo "Done. Review the changes in:" +echo " $WIT_DIR/deps/" +echo " $TYPES_DIR/"