diff --git a/README.md b/README.md index 263b0bc..9c5ab25 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Plugins are sandboxed by default - they cannot access the filesystem or network Plugins like `ls` or `cat` can interact with the filesystem using the primitives of the languages they are written in. - on the CLI, a folder from the disk is mounted via the `--dir` flag -- on the browser, a virtual filesystem is mounted, the I/O operations are forwarded via the `@bytecodealliance/preview2-shim/filesystem` shim, which shims the `wasi:filesystem` filesystem interface +- on the browser, a virtual filesystem is mounted, the I/O operations are forwarded via a [local fork](./packages/web-host/overrides/@bytecodealliance/preview2-shim) of `@bytecodealliance/preview2-shim/filesystem` shim, which shims the `wasi:filesystem` filesystem interface

Demo

@@ -410,6 +410,13 @@ pluginlab\ - formating the rust code - typechecking the TypeScript code +### Local fork of `@bytecodealliance/preview2-shim` + +- The original [`@bytecodealliance/preview2-shim`](https://github.com/bytecodealliance/jco/tree/main/packages/preview2-shim) for the **browser** doesn't support **WRITE** operations on the filesystem +- A fork was created in [`packages/web-host/overrides/@bytecodealliance/preview2-shim`](./packages/web-host/overrides/@bytecodealliance/preview2-shim) to fix this issue + +Everything is described in [PR#15 Support plugin-tee in the web host](https://github.com/topheman/webassembly-component-model-experiments/pull/15) (must read 😉). + ## Resources ### Optional tools diff --git a/biome.json b/biome.json index f016099..9969565 100644 --- a/biome.json +++ b/biome.json @@ -6,7 +6,8 @@ "!**/node_modules", "!**/dist", "!**/build", - "!**/package.json" + "!**/package.json", + "!packages/web-host/overrides/**" ] }, "formatter": { diff --git a/lint-staged.config.mjs b/lint-staged.config.mjs index 2a5e379..8447926 100644 --- a/lint-staged.config.mjs +++ b/lint-staged.config.mjs @@ -1,6 +1,6 @@ export default { - 'packages/**': 'biome check --write', + 'packages/*/!(overrides)/**': 'biome check --write', 'packages/plugin-echo/**/*.ts': () => 'npm run typecheck --workspace=packages/plugin-echo', 'packages/web-host/**/*.{ts,tsx}': () => 'npm run typecheck --workspace=packages/web-host', // `cargo fmt doesn't accept files diff --git a/packages/web-host/clis/prepareFilesystem.ts b/packages/web-host/clis/prepareFilesystem.ts index 7d1a3fc..ab748da 100755 --- a/packages/web-host/clis/prepareFilesystem.ts +++ b/packages/web-host/clis/prepareFilesystem.ts @@ -53,6 +53,18 @@ function assertPathIsString(path: string): asserts path is string { } } +function template(data: VirtualFs): string { + return `// THIS FILE IS GENERATED BY THE prepareVirtualFs COMMAND, DO NOT EDIT IT MANUALLY + +// It is meant to be used for mounting a virtual filesystem in the browser +// interacting with @bytecodealliance/preview2-shim/filesystem , the shim for wasi:filesystem +// +// The \`fs\` calls like \`read\`, \`readDir\` ... in rust or other languages will be redirected to this virtual filesystem +// and will interact as if the filesystem was a real one. + +export function makeVirtualFs() { return ${JSON.stringify(data, null, 2)}; }`; +} + function run() { program .description("Prepare wasm files for the web host") @@ -65,17 +77,7 @@ function run() { if (options.format === "json") { console.log(JSON.stringify(virtualFs, null, 2)); } else if (options.format === "ts") { - console.log( - `// THIS FILE IS GENERATED BY THE prepareVirtualFs COMMAND, DO NOT EDIT IT MANUALLY - -// It is meant to be used for mounting a virtual filesystem in the browser -// interacting with @bytecodealliance/preview2-shim/filesystem , the shim for wasi:filesystem -// -// The \`fs\` calls like \`read\`, \`readDir\` ... in rust or other languages will be redirected to this virtual filesystem -// and will interact as if the filesystem was a real one. - -export function makeVirtualFs() { return ${JSON.stringify(virtualFs, null, 2)}; }`, - ); + console.log(template(virtualFs)); } }); diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/README.md b/packages/web-host/overrides/@bytecodealliance/preview2-shim/README.md new file mode 100644 index 0000000..6a1b6f8 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/README.md @@ -0,0 +1,18 @@ +# Preview2 Shim + +WASI Preview2 implementations for Node.js & browsers. + +Node.js support is fully tested and conformant against the Wasmtime test suite. + +Browser support is considered experimental, and not currently suitable for production applications. + +# License + +This project is licensed under the Apache 2.0 license with the LLVM exception. +See [LICENSE](LICENSE) for more details. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally submitted +for inclusion in this project by you, as defined in the Apache-2.0 license, +shall be licensed as above, without any additional terms or conditions. diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/cli.js b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/cli.js new file mode 100644 index 0000000..a265411 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/cli.js @@ -0,0 +1,162 @@ +import { _setCwd as fsSetCwd } from './filesystem.js'; +import { streams } from './io.js'; +const { InputStream, OutputStream } = streams; + +const symbolDispose = Symbol.dispose ?? Symbol.for('dispose'); + +let _env = [], _args = [], _cwd = "/"; +export function _setEnv (envObj) { + _env = Object.entries(envObj); +} +export function _setArgs (args) { + _args = args; +} + +export function _setCwd (cwd) { + fsSetCwd(_cwd = cwd); +} + +export const environment = { + getEnvironment () { + return _env; + }, + getArguments () { + return _args; + }, + initialCwd () { + return _cwd; + } +}; + +class ComponentExit extends Error { + constructor(code) { + super(`Component exited ${code === 0 ? 'successfully' : 'with error'}`); + this.exitError = true; + this.code = code; + } +} + +export const exit = { + exit (status) { + throw new ComponentExit(status.tag === 'err' ? 1 : 0); + }, + exitWithCode (code) { + throw new ComponentExit(code); + } +}; + +/** + * @param {import('../common/io.js').InputStreamHandler} handler + */ +export function _setStdin (handler) { + stdinStream.handler = handler; +} +/** + * @param {import('../common/io.js').OutputStreamHandler} handler + */ +export function _setStderr (handler) { + stderrStream.handler = handler; +} +/** + * @param {import('../common/io.js').OutputStreamHandler} handler + */ +export function _setStdout (handler) { + stdoutStream.handler = handler; +} + +const stdinStream = new InputStream({ + blockingRead (_len) { + // TODO + }, + subscribe () { + // TODO + }, + [symbolDispose] () { + // TODO + } +}); +let textDecoder = new TextDecoder(); +const stdoutStream = new OutputStream({ + write (contents) { + if (contents[contents.length - 1] == 10) { + // console.log already appends a new line + contents = contents.subarray(0, contents.length - 1); + } + console.log(textDecoder.decode(contents)); + }, + blockingFlush () { + }, + [symbolDispose] () { + } +}); +const stderrStream = new OutputStream({ + write (contents) { + if (contents[contents.length - 1] == 10) { + // console.error already appends a new line + contents = contents.subarray(0, contents.length - 1); + } + console.error(textDecoder.decode(contents)); + }, + blockingFlush () { + }, + [symbolDispose] () { + + } +}); + +export const stdin = { + InputStream, + getStdin () { + return stdinStream; + } +}; + +export const stdout = { + OutputStream, + getStdout () { + return stdoutStream; + } +}; + +export const stderr = { + OutputStream, + getStderr () { + return stderrStream; + } +}; + +class TerminalInput {} +class TerminalOutput {} + +const terminalStdoutInstance = new TerminalOutput(); +const terminalStderrInstance = new TerminalOutput(); +const terminalStdinInstance = new TerminalInput(); + +export const terminalInput = { + TerminalInput +}; + +export const terminalOutput = { + TerminalOutput +}; + +export const terminalStderr = { + TerminalOutput, + getTerminalStderr () { + return terminalStderrInstance; + } +}; + +export const terminalStdin = { + TerminalInput, + getTerminalStdin () { + return terminalStdinInstance; + } +}; + +export const terminalStdout = { + TerminalOutput, + getTerminalStdout () { + return terminalStdoutInstance; + } +}; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/clocks.js b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/clocks.js new file mode 100644 index 0000000..0a0b7bd --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/clocks.js @@ -0,0 +1,34 @@ +export const monotonicClock = { + resolution() { + // usually we dont get sub-millisecond accuracy in the browser + // Note: is there a better way to determine this? + return 1e6; + }, + now () { + // performance.now() is in milliseconds, but we want nanoseconds + return BigInt(Math.floor(performance.now() * 1e6)); + }, + subscribeInstant (instant) { + instant = BigInt(instant); + const now = this.now(); + if (instant <= now) + return this.subscribeDuration(0); + return this.subscribeDuration(instant - now); + }, + subscribeDuration (_duration) { + _duration = BigInt(_duration); + console.log(`[monotonic-clock] subscribe`); + } +}; + +export const wallClock = { + now() { + let now = Date.now(); // in milliseconds + const seconds = BigInt(Math.floor(now / 1e3)); + const nanoseconds = (now % 1e3) * 1e6; + return { seconds, nanoseconds }; + }, + resolution() { + return { seconds: 0n, nanoseconds: 1e6 }; + } +}; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/filesystem.js b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/filesystem.js new file mode 100644 index 0000000..16784d2 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/filesystem.js @@ -0,0 +1,295 @@ +import { streams } from './io.js'; +import { environment } from './cli.js'; + +const { InputStream, OutputStream } = streams; + +let _cwd = "/"; + +export function _setCwd (cwd) { + _cwd = cwd; +} + +export function _setFileData (fileData) { + _fileData = fileData; + _rootPreopen[0] = new Descriptor(fileData); + const cwd = environment.initialCwd(); + _setCwd(cwd || '/'); +} + +export function _getFileData () { + return JSON.stringify(_fileData); +} + +let _fileData = { dir: {} }; + +const timeZero = { + seconds: BigInt(0), + nanoseconds: 0 +}; + +function getChildEntry (parentEntry, subpath, openFlags) { + if (subpath === '.' && _rootPreopen && descriptorGetEntry(_rootPreopen[0]) === parentEntry) { + subpath = _cwd; + if (subpath.startsWith('/') && subpath !== '/') + subpath = subpath.slice(1); + } + let entry = parentEntry; + let segmentIdx; + do { + if (!entry || !entry.dir) throw 'not-directory'; + segmentIdx = subpath.indexOf('/'); + const segment = segmentIdx === -1 ? subpath : subpath.slice(0, segmentIdx); + if (segment === '..') throw 'no-entry'; + if (segment === '.' || segment === ''); + else if (!entry.dir[segment] && openFlags.create) + entry = entry.dir[segment] = openFlags.directory ? { dir: {} } : { source: new Uint8Array([]) }; + else + entry = entry.dir[segment]; + subpath = subpath.slice(segmentIdx + 1); + } while (segmentIdx !== -1) + if (!entry) throw 'no-entry'; + return entry; +} + +function getSource (fileEntry) { + if (typeof fileEntry.source === 'string') { + fileEntry.source = new TextEncoder().encode(fileEntry.source); + } + return fileEntry.source; +} + +class DirectoryEntryStream { + constructor (entries) { + this.idx = 0; + this.entries = entries; + } + readDirectoryEntry () { + if (this.idx === this.entries.length) + return null; + const [name, entry] = this.entries[this.idx]; + this.idx += 1; + return { + name, + type: entry.dir ? 'directory' : 'regular-file' + }; + } +} + +class Descriptor { + #stream; + #entry; + #mtime = 0; + + _getEntry (descriptor) { + return descriptor.#entry; + } + + constructor (entry, isStream) { + if (isStream) + this.#stream = entry; + else + this.#entry = entry; + } + + readViaStream(_offset) { + const source = getSource(this.#entry); + let offset = Number(_offset); + return new InputStream({ + blockingRead (len) { + if (offset === source.byteLength) + throw { tag: 'closed' }; + const bytes = source.slice(offset, offset + Number(len)); + offset += bytes.byteLength; + return bytes; + } + }); + } + + writeViaStream(_offset) { + const entry = this.#entry; + let offset = Number(_offset); + return new OutputStream({ + write (buf) { + const newSource = new Uint8Array(buf.byteLength); + newSource.set(buf, 0); + offset += buf.byteLength; + entry.source = newSource; + return buf.byteLength; + } + }); + } + + appendViaStream() { + const entry = this.#entry; + return new OutputStream({ + write (buf) { + const entrySource = getSource(entry); + const newSource = new Uint8Array(buf.byteLength + entrySource.byteLength); + newSource.set(entrySource, 0); + newSource.set(buf, entrySource.byteLength); + entry.source = newSource; + return buf.byteLength; + } + }); + } + + advise(descriptor, offset, length, advice) { + console.log(`[filesystem] ADVISE`, descriptor, offset, length, advice); + } + + syncData() { + console.log(`[filesystem] SYNC DATA`); + } + + getFlags() { + console.log(`[filesystem] FLAGS FOR`); + } + + getType() { + if (this.#stream) return 'fifo'; + if (this.#entry.dir) return 'directory'; + if (this.#entry.source) return 'regular-file'; + return 'unknown'; + } + + setSize(size) { + console.log(`[filesystem] SET SIZE`, size); + } + + setTimes(dataAccessTimestamp, dataModificationTimestamp) { + console.log(`[filesystem] SET TIMES`, dataAccessTimestamp, dataModificationTimestamp); + } + + read(length, offset) { + const source = getSource(this.#entry); + return [source.slice(offset, offset + length), offset + length >= source.byteLength]; + } + + write(buffer, offset) { + if (offset !== 0) throw 'invalid-seek'; + this.#entry.source = buffer; + return buffer.byteLength; + } + + readDirectory() { + if (!this.#entry?.dir) + throw 'bad-descriptor'; + return new DirectoryEntryStream(Object.entries(this.#entry.dir).sort(([a], [b]) => a > b ? 1 : -1)); + } + + sync() { + console.log(`[filesystem] SYNC`); + } + + createDirectoryAt(path) { + const entry = getChildEntry(this.#entry, path, { create: true, directory: true }); + if (entry.source) throw 'exist'; + } + + stat() { + let type = 'unknown', size = BigInt(0); + if (this.#entry.source) { + type = 'regular-file'; + const source = getSource(this.#entry); + size = BigInt(source.byteLength); + } + else if (this.#entry.dir) { + type = 'directory'; + } + return { + type, + linkCount: BigInt(0), + size, + dataAccessTimestamp: timeZero, + dataModificationTimestamp: timeZero, + statusChangeTimestamp: timeZero, + } + } + + statAt(_pathFlags, path) { + const entry = getChildEntry(this.#entry, path, { create: false, directory: false }); + let type = 'unknown', size = BigInt(0); + if (entry.source) { + type = 'regular-file'; + const source = getSource(entry); + size = BigInt(source.byteLength); + } + else if (entry.dir) { + type = 'directory'; + } + return { + type, + linkCount: BigInt(0), + size, + dataAccessTimestamp: timeZero, + dataModificationTimestamp: timeZero, + statusChangeTimestamp: timeZero, + }; + } + + setTimesAt() { + console.log(`[filesystem] SET TIMES AT`); + } + + linkAt() { + console.log(`[filesystem] LINK AT`); + } + + openAt(_pathFlags, path, openFlags, _descriptorFlags, _modes) { + const childEntry = getChildEntry(this.#entry, path, openFlags); + return new Descriptor(childEntry); + } + + readlinkAt() { + console.log(`[filesystem] READLINK AT`); + } + + removeDirectoryAt() { + console.log(`[filesystem] REMOVE DIR AT`); + } + + renameAt() { + console.log(`[filesystem] RENAME AT`); + } + + symlinkAt() { + console.log(`[filesystem] SYMLINK AT`); + } + + unlinkFileAt() { + console.log(`[filesystem] UNLINK FILE AT`); + } + + isSameObject(other) { + return other === this; + } + + metadataHash() { + let upper = BigInt(0); + upper += BigInt(this.#mtime); + return { upper, lower: BigInt(0) }; + } + + metadataHashAt(_pathFlags, _path) { + let upper = BigInt(0); + upper += BigInt(this.#mtime); + return { upper, lower: BigInt(0) }; + } +} +const descriptorGetEntry = Descriptor.prototype._getEntry; +delete Descriptor.prototype._getEntry; + +let _preopens = [[new Descriptor(_fileData), '/']], _rootPreopen = _preopens[0]; + +export const preopens = { + getDirectories () { + return _preopens; + } +} + +export const types = { + Descriptor, + DirectoryEntryStream +}; + +export { types as filesystemTypes } diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/http.js b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/http.js new file mode 100644 index 0000000..4a1b244 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/http.js @@ -0,0 +1,144 @@ +/** + * @param {import("../../types/interfaces/wasi-http-types").Request} req + * @returns {string} + */ +export function send(req) { + console.log(`[http] Send (browser) ${req.uri}`); + try { + const xhr = new XMLHttpRequest(); + xhr.open(req.method.toString(), req.uri, false); + const requestHeaders = new Headers(req.headers); + for (let [name, value] of requestHeaders.entries()) { + if (name !== "user-agent" && name !== "host") { + xhr.setRequestHeader(name, value); + } + } + xhr.send(req.body && req.body.length > 0 ? req.body : null); + const body = xhr.response ? new TextEncoder().encode(xhr.response) : undefined; + const headers = []; + xhr.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach((line) => { + var parts = line.split(': '); + var key = parts.shift(); + var value = parts.join(': '); + headers.push([key, value]); + }); + return { + status: xhr.status, + headers, + body, + }; + } catch (err) { + throw new Error(err.message); + } +} + +export const incomingHandler = { + handle () { + + } +}; + +export const outgoingHandler = { + handle () { + + } +}; + +export const types = { + dropFields(_fields) { + console.log("[types] Drop fields"); + }, + newFields(_entries) { + console.log("[types] New fields"); + }, + fieldsGet(_fields, _name) { + console.log("[types] Fields get"); + }, + fieldsSet(_fields, _name, _value) { + console.log("[types] Fields set"); + }, + fieldsDelete(_fields, _name) { + console.log("[types] Fields delete"); + }, + fieldsAppend(_fields, _name, _value) { + console.log("[types] Fields append"); + }, + fieldsEntries(_fields) { + console.log("[types] Fields entries"); + }, + fieldsClone(_fields) { + console.log("[types] Fields clone"); + }, + finishIncomingStream(s) { + console.log(`[types] Finish incoming stream ${s}`); + }, + finishOutgoingStream(s, _trailers) { + console.log(`[types] Finish outgoing stream ${s}`); + }, + dropIncomingRequest(_req) { + console.log("[types] Drop incoming request"); + }, + dropOutgoingRequest(_req) { + console.log("[types] Drop outgoing request"); + }, + incomingRequestMethod(_req) { + console.log("[types] Incoming request method"); + }, + incomingRequestPathWithQuery(_req) { + console.log("[types] Incoming request path with query"); + }, + incomingRequestScheme(_req) { + console.log("[types] Incoming request scheme"); + }, + incomingRequestAuthority(_req) { + console.log("[types] Incoming request authority"); + }, + incomingRequestHeaders(_req) { + console.log("[types] Incoming request headers"); + }, + incomingRequestConsume(_req) { + console.log("[types] Incoming request consume"); + }, + newOutgoingRequest(_method, _pathWithQuery, _scheme, _authority, _headers) { + console.log("[types] New outgoing request"); + }, + outgoingRequestWrite(_req) { + console.log("[types] Outgoing request write"); + }, + dropResponseOutparam(_res) { + console.log("[types] Drop response outparam"); + }, + setResponseOutparam(_response) { + console.log("[types] Drop fields"); + }, + dropIncomingResponse(_res) { + console.log("[types] Drop incoming response"); + }, + dropOutgoingResponse(_res) { + console.log("[types] Drop outgoing response"); + }, + incomingResponseStatus(_res) { + console.log("[types] Incoming response status"); + }, + incomingResponseHeaders(_res) { + console.log("[types] Incoming response headers"); + }, + incomingResponseConsume(_res) { + console.log("[types] Incoming response consume"); + }, + newOutgoingResponse(_statusCode, _headers) { + console.log("[types] New outgoing response"); + }, + outgoingResponseWrite(_res) { + console.log("[types] Outgoing response write"); + }, + dropFutureIncomingResponse(_f) { + console.log("[types] Drop future incoming response"); + }, + futureIncomingResponseGet(_f) { + console.log("[types] Future incoming response get"); + }, + listenToFutureIncomingResponse(_f) { + console.log("[types] Listen to future incoming response"); + } +}; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/index.js b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/index.js new file mode 100644 index 0000000..da23919 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/index.js @@ -0,0 +1,17 @@ +import * as clocks from "./clocks.js"; +import * as filesystem from "./filesystem.js"; +import * as http from "./http.js"; +import * as io from "./io.js"; +import * as random from "./random.js"; +import * as sockets from "./sockets.js"; +import * as cli from "./cli.js"; + +export { + clocks, + filesystem, + http, + io, + random, + sockets, + cli, +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/io.js b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/io.js new file mode 100644 index 0000000..b450678 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/io.js @@ -0,0 +1,185 @@ +let id = 0; + +const symbolDispose = Symbol.dispose || Symbol.for('dispose'); + +const IoError = class Error { + constructor (msg) { + this.msg = msg; + } + toDebugString () { + return this.msg; + } +} + +/** + * @typedef {{ + * read?: (len: BigInt) => Uint8Array, + * blockingRead: (len: BigInt) => Uint8Array, + * skip?: (len: BigInt) => BigInt, + * blockingSkip?: (len: BigInt) => BigInt, + * subscribe: () => void, + * drop?: () => void, + * }} InputStreamHandler + * + * @typedef {{ + * checkWrite?: () -> BigInt, + * write: (buf: Uint8Array) => BigInt, + * blockingWriteAndFlush?: (buf: Uint8Array) => void, + * flush?: () => void, + * blockingFlush: () => void, + * writeZeroes?: (len: BigInt) => void, + * blockingWriteZeroes?: (len: BigInt) => void, + * blockingWriteZeroesAndFlush?: (len: BigInt) => void, + * splice?: (src: InputStream, len: BigInt) => BigInt, + * blockingSplice?: (src: InputStream, len: BigInt) => BigInt, + * forward?: (src: InputStream) => void, + * subscribe?: () => void, + * drop?: () => void, + * }} OutputStreamHandler + * + **/ + +class InputStream { + /** + * @param {InputStreamHandler} handler + */ + constructor (handler) { + if (!handler) + console.trace('no handler'); + this.id = ++id; + this.handler = handler; + } + read(len) { + if (this.handler.read) + return this.handler.read(len); + return this.handler.blockingRead.call(this, len); + } + blockingRead(len) { + return this.handler.blockingRead.call(this, len); + } + skip(len) { + if (this.handler.skip) + return this.handler.skip.call(this, len); + if (this.handler.read) { + const bytes = this.handler.read.call(this, len); + return BigInt(bytes.byteLength); + } + return this.blockingSkip.call(this, len); + } + blockingSkip(len) { + if (this.handler.blockingSkip) + return this.handler.blockingSkip.call(this, len); + const bytes = this.handler.blockingRead.call(this, len); + return BigInt(bytes.byteLength); + } + subscribe() { + console.log(`[streams] Subscribe to input stream ${this.id}`); + } + [symbolDispose] () { + if (this.handler.drop) + this.handler.drop.call(this); + } +} + +class OutputStream { + /** + * @param {OutputStreamHandler} handler + */ + constructor (handler) { + if (!handler) + console.trace('no handler'); + this.id = ++id; + this.open = true; + this.handler = handler; + } + checkWrite(len) { + if (!this.open) + return 0n; + if (this.handler.checkWrite) + return this.handler.checkWrite.call(this, len); + return 1_000_000n; + } + write(buf) { + this.handler.write.call(this, buf); + } + blockingWriteAndFlush(buf) { + /// Perform a write of up to 4096 bytes, and then flush the stream. Block + /// until all of these operations are complete, or an error occurs. + /// + /// This is a convenience wrapper around the use of `check-write`, + /// `subscribe`, `write`, and `flush`, and is implemented with the + /// following pseudo-code: + /// + /// ```text + /// let pollable = this.subscribe(); + /// while !contents.is_empty() { + /// // Wait for the stream to become writable + /// poll-one(pollable); + /// let Ok(n) = this.check-write(); // eliding error handling + /// let len = min(n, contents.len()); + /// let (chunk, rest) = contents.split_at(len); + /// this.write(chunk ); // eliding error handling + /// contents = rest; + /// } + /// this.flush(); + /// // Wait for completion of `flush` + /// poll-one(pollable); + /// // Check for any errors that arose during `flush` + /// let _ = this.check-write(); // eliding error handling + /// ``` + this.handler.write.call(this, buf); + } + flush() { + if (this.handler.flush) + this.handler.flush.call(this); + } + blockingFlush() { + this.open = true; + } + writeZeroes(len) { + this.write.call(this, new Uint8Array(Number(len))); + } + blockingWriteZeroes(len) { + this.blockingWrite.call(this, new Uint8Array(Number(len))); + } + blockingWriteZeroesAndFlush(len) { + this.blockingWriteAndFlush.call(this, new Uint8Array(Number(len))); + } + splice(src, len) { + const spliceLen = Math.min(len, this.checkWrite.call(this)); + const bytes = src.read(spliceLen); + this.write.call(this, bytes); + return bytes.byteLength; + } + blockingSplice(_src, _len) { + console.log(`[streams] Blocking splice ${this.id}`); + } + forward(_src) { + console.log(`[streams] Forward ${this.id}`); + } + subscribe() { + console.log(`[streams] Subscribe to output stream ${this.id}`); + } + [symbolDispose]() { + } +} + +export const error = { Error: IoError }; + +export const streams = { InputStream, OutputStream }; + +class Pollable {} + +function pollList (_list) { + // TODO +} + +function pollOne (_poll) { + // TODO +} + +export const poll = { + Pollable, + pollList, + pollOne +}; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/random.js b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/random.js new file mode 100644 index 0000000..0c47385 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/random.js @@ -0,0 +1,56 @@ +const MAX_BYTES = 65536; + +let insecureRandomValue1, insecureRandomValue2; + +export const insecure = { + getInsecureRandomBytes (len) { + return random.getRandomBytes(len); + }, + getInsecureRandomU64 () { + return random.getRandomU64(); + } +}; + +let insecureSeedValue1, insecureSeedValue2; + +export const insecureSeed = { + insecureSeed () { + if (insecureSeedValue1 === undefined) { + insecureSeedValue1 = random.getRandomU64(); + insecureSeedValue2 = random.getRandomU64(); + } + return [insecureSeedValue1, insecureSeedValue2]; + } +}; + +export const random = { + getRandomBytes(len) { + const bytes = new Uint8Array(Number(len)); + + if (len > MAX_BYTES) { + // this is the max bytes crypto.getRandomValues + // can do at once see https://developer.mozilla.org/en-US/docs/Web/API/window.crypto.getRandomValues + for (var generated = 0; generated < len; generated += MAX_BYTES) { + // buffer.slice automatically checks if the end is past the end of + // the buffer so we don't have to here + crypto.getRandomValues(bytes.subarray(generated, generated + MAX_BYTES)); + } + } else { + crypto.getRandomValues(bytes); + } + + return bytes; + }, + + getRandomU64 () { + return crypto.getRandomValues(new BigUint64Array(1))[0]; + }, + + insecureRandom () { + if (insecureRandomValue1 === undefined) { + insecureRandomValue1 = random.getRandomU64(); + insecureRandomValue2 = random.getRandomU64(); + } + return [insecureRandomValue1, insecureRandomValue2]; + } +}; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/sockets.js b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/sockets.js new file mode 100644 index 0000000..6dda749 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/lib/browser/sockets.js @@ -0,0 +1,186 @@ +export const instanceNetwork = { + instanceNetwork () { + console.log(`[sockets] instance network`); + } +}; + +export const ipNameLookup = { + dropResolveAddressStream () { + + }, + subscribe () { + + }, + resolveAddresses () { + + }, + resolveNextAddress () { + + }, + nonBlocking () { + + }, + setNonBlocking () { + + }, +}; + +export const network = { + dropNetwork () { + + } +}; + +export const tcpCreateSocket = { + createTcpSocket () { + + } +}; + +export const tcp = { + subscribe () { + + }, + dropTcpSocket() { + + }, + bind() { + + }, + connect() { + + }, + listen() { + + }, + accept() { + + }, + localAddress() { + + }, + remoteAddress() { + + }, + addressFamily() { + + }, + setListenBacklogSize() { + + }, + keepAlive() { + + }, + setKeepAlive() { + + }, + noDelay() { + + }, + setNoDelay() { + + }, + unicastHopLimit() { + + }, + setUnicastHopLimit() { + + }, + receiveBufferSize() { + + }, + setReceiveBufferSize() { + + }, + sendBufferSize() { + + }, + setSendBufferSize() { + + }, + nonBlocking() { + + }, + setNonBlocking() { + + }, + shutdown() { + + } +}; + +export const udp = { + subscribe () { + + }, + + dropUdpSocket () { + + }, + + bind () { + + }, + + connect () { + + }, + + receive () { + + }, + + send () { + + }, + + localAddress () { + + }, + + remoteAddress () { + + }, + + addressFamily () { + + }, + + unicastHopLimit () { + + }, + + setUnicastHopLimit () { + + }, + + receiveBufferSize () { + + }, + + setReceiveBufferSize () { + + }, + + sendBufferSize () { + + }, + + setSendBufferSize () { + + }, + + nonBlocking () { + + }, + + setNonBlocking () { + + } +}; + +export const udpCreateSocket = { + createUdpSocket () { + + } +}; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/package.json b/packages/web-host/overrides/@bytecodealliance/preview2-shim/package.json new file mode 100644 index 0000000..ad0f53d --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/package.json @@ -0,0 +1,39 @@ +{ + "name": "@bytecodealliance/preview2-shim", + "version": "0.17.2", + "description": "WASI Preview2 shim for JS environments", + "author": "Guy Bedford, Eduardo Rodrigues<16357187+eduardomourar@users.noreply.github.com>", + "type": "module", + "types": "./types/index.d.ts", + "exports": { + ".": { + "types": "./types/index.d.ts", + "node": "./lib/nodejs/index.js", + "default": "./lib/browser/index.js" + }, + "./*": { + "types": "./types/*.d.ts", + "node": "./lib/nodejs/*.js", + "default": "./lib/browser/*.js" + } + }, + "scripts": { + "test": "node --expose-gc ../../node_modules/mocha/bin/mocha.js -u tdd test/test.js --timeout 30000" + }, + "files": [ + "types", + "lib" + ], + "devDependencies": { + "mocha": "^10.2.0" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/bytecodealliance/jco.git" + }, + "license": "(Apache-2.0 WITH LLVM-exception)", + "bugs": { + "url": "https://github.com/bytecodealliance/jco/issues" + }, + "homepage": "https://github.com/bytecodealliance/jco#readme" +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/cli.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/cli.d.ts new file mode 100644 index 0000000..25c66d7 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/cli.d.ts @@ -0,0 +1,11 @@ +export type * as environment from './interfaces/wasi-cli-environment.d.ts'; +export type * as exit from './interfaces/wasi-cli-exit.d.ts'; +export type * as run from './interfaces/wasi-cli-run.d.ts'; +export type * as stderr from './interfaces/wasi-cli-stderr.d.ts'; +export type * as stdin from './interfaces/wasi-cli-stdin.d.ts'; +export type * as stdout from './interfaces/wasi-cli-stdout.d.ts'; +export type * as terminalInput from './interfaces/wasi-cli-terminal-input.d.ts'; +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'; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/clocks.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/clocks.d.ts new file mode 100644 index 0000000..5924b07 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/clocks.d.ts @@ -0,0 +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'; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/filesystem.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/filesystem.d.ts new file mode 100644 index 0000000..0780816 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/filesystem.d.ts @@ -0,0 +1,2 @@ +export type * as preopens from './interfaces/wasi-filesystem-preopens.d.ts'; +export type * as types from './interfaces/wasi-filesystem-types.d.ts'; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/http.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/http.d.ts new file mode 100644 index 0000000..909d354 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/http.d.ts @@ -0,0 +1,3 @@ +export type * as incomingHandler from './interfaces/wasi-http-incoming-handler.d.ts'; +export type * as outgoingHandler from './interfaces/wasi-http-outgoing-handler.d.ts'; +export type * as types from './interfaces/wasi-http-types.d.ts'; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/index.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/index.d.ts new file mode 100644 index 0000000..aeed2d5 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/index.d.ts @@ -0,0 +1,7 @@ +export type * as cli from "./cli.d.ts"; +export type * as clocks from './clocks.d.ts'; +export type * as filesystem from './filesystem.d.ts'; +export type * as http from "./http.d.ts"; +export type * as io from "./io.d.ts"; +export type * as random from "./random.d.ts"; +export type * as sockets from "./sockets.d.ts"; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-environment.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-environment.d.ts new file mode 100644 index 0000000..45f685c --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-environment.d.ts @@ -0,0 +1,21 @@ +/** @module Interface wasi:cli/environment@0.2.3 **/ +/** + * 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. + */ +export function getEnvironment(): Array<[string, string]>; +/** + * Get the POSIX-style arguments to the program. + */ +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; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-exit.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-exit.d.ts new file mode 100644 index 0000000..5ae4465 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-exit.d.ts @@ -0,0 +1,6 @@ +/** @module Interface wasi:cli/exit@0.2.3 **/ +/** + * Exit the current instance and any linked instances. + */ +export function exit(status: Result): void; +export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-run.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-run.d.ts new file mode 100644 index 0000000..5f604f5 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-run.d.ts @@ -0,0 +1,5 @@ +/** @module Interface wasi:cli/run@0.2.3 **/ +/** + * Run the program. + */ +export function run(): void; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stderr.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stderr.d.ts new file mode 100644 index 0000000..4439d41 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stderr.d.ts @@ -0,0 +1,3 @@ +/** @module Interface wasi:cli/stderr@0.2.3 **/ +export function getStderr(): OutputStream; +export type OutputStream = import('./wasi-io-streams.js').OutputStream; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stdin.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stdin.d.ts new file mode 100644 index 0000000..96b7108 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stdin.d.ts @@ -0,0 +1,3 @@ +/** @module Interface wasi:cli/stdin@0.2.3 **/ +export function getStdin(): InputStream; +export type InputStream = import('./wasi-io-streams.js').InputStream; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stdout.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stdout.d.ts new file mode 100644 index 0000000..f4ba8ce --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-stdout.d.ts @@ -0,0 +1,3 @@ +/** @module Interface wasi:cli/stdout@0.2.3 **/ +export function getStdout(): OutputStream; +export type OutputStream = import('./wasi-io-streams.js').OutputStream; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-input.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-input.d.ts new file mode 100644 index 0000000..12f18cc --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-input.d.ts @@ -0,0 +1,8 @@ +/** @module Interface wasi:cli/terminal-input@0.2.3 **/ + +export class TerminalInput { + /** + * This type does not have a public constructor. + */ + private constructor(); +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-output.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-output.d.ts new file mode 100644 index 0000000..43f9b0f --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-output.d.ts @@ -0,0 +1,8 @@ +/** @module Interface wasi:cli/terminal-output@0.2.3 **/ + +export class TerminalOutput { + /** + * This type does not have a public constructor. + */ + private constructor(); +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stderr.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stderr.d.ts new file mode 100644 index 0000000..0b01576 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stderr.d.ts @@ -0,0 +1,7 @@ +/** @module Interface wasi:cli/terminal-stderr@0.2.3 **/ +/** + * If stderr is connected to a terminal, return a `terminal-output` handle + * allowing further interaction with it. + */ +export function getTerminalStderr(): TerminalOutput | undefined; +export type TerminalOutput = import('./wasi-cli-terminal-output.js').TerminalOutput; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stdin.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stdin.d.ts new file mode 100644 index 0000000..c01afc5 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stdin.d.ts @@ -0,0 +1,7 @@ +/** @module Interface wasi:cli/terminal-stdin@0.2.3 **/ +/** + * If stdin is connected to a terminal, return a `terminal-input` handle + * allowing further interaction with it. + */ +export function getTerminalStdin(): TerminalInput | undefined; +export type TerminalInput = import('./wasi-cli-terminal-input.js').TerminalInput; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stdout.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stdout.d.ts new file mode 100644 index 0000000..6dc6e92 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-cli-terminal-stdout.d.ts @@ -0,0 +1,7 @@ +/** @module Interface wasi:cli/terminal-stdout@0.2.3 **/ +/** + * If stdout is connected to a terminal, return a `terminal-output` handle + * allowing further interaction with it. + */ +export function getTerminalStdout(): TerminalOutput | undefined; +export type TerminalOutput = import('./wasi-cli-terminal-output.js').TerminalOutput; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-clocks-monotonic-clock.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-clocks-monotonic-clock.d.ts new file mode 100644 index 0000000..5a6ec46 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-clocks-monotonic-clock.d.ts @@ -0,0 +1,34 @@ +/** @module Interface wasi:clocks/monotonic-clock@0.2.3 **/ +/** + * Read the current value of the clock. + * + * The clock is monotonic, therefore calling this function repeatedly will + * produce a sequence of non-decreasing values. + */ +export function now(): Instant; +/** + * Query the resolution of the clock. Returns the duration of time + * corresponding to a clock tick. + */ +export function resolution(): Duration; +/** + * Create a `pollable` which will resolve once the specified instant + * has occurred. + */ +export function subscribeInstant(when: Instant): Pollable; +/** + * Create a `pollable` that will resolve after the specified duration has + * elapsed from the time this function is invoked. + */ +export function subscribeDuration(when: Duration): Pollable; +export type Pollable = import('./wasi-io-poll.js').Pollable; +/** + * An instant in time, in nanoseconds. An instant is relative to an + * unspecified initial value, and can only be compared to instances from + * the same monotonic-clock. + */ +export type Instant = bigint; +/** + * A duration of time, in nanoseconds. + */ +export type Duration = bigint; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-clocks-wall-clock.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-clocks-wall-clock.d.ts new file mode 100644 index 0000000..9310603 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-clocks-wall-clock.d.ts @@ -0,0 +1,30 @@ +/** @module Interface wasi:clocks/wall-clock@0.2.3 **/ +/** + * 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/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-filesystem-preopens.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-filesystem-preopens.d.ts new file mode 100644 index 0000000..cca6e6b --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-filesystem-preopens.d.ts @@ -0,0 +1,6 @@ +/** @module Interface wasi:filesystem/preopens@0.2.3 **/ +/** + * Return the set of preopened directories, and their paths. + */ +export function getDirectories(): Array<[Descriptor, string]>; +export type Descriptor = import('./wasi-filesystem-types.js').Descriptor; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-filesystem-types.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-filesystem-types.d.ts new file mode 100644 index 0000000..cf1f580 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-filesystem-types.d.ts @@ -0,0 +1,669 @@ +/** @module Interface wasi:filesystem/types@0.2.3 **/ +/** + * Attempts to extract a filesystem-related `error-code` from the stream + * `error` provided. + * + * Stream operations which return `stream-error::last-operation-failed` + * have a payload with more information about the operation that failed. + * This payload can be passed through to this function to see if there's + * filesystem-related information about the error to return. + * + * Note that this function is fallible because not all stream-related + * errors are filesystem-related errors. + */ +export function filesystemErrorCode(err: Error): ErrorCode | undefined; +export type InputStream = import('./wasi-io-streams.js').InputStream; +export type OutputStream = import('./wasi-io-streams.js').OutputStream; +export type Error = import('./wasi-io-streams.js').Error; +export type Datetime = import('./wasi-clocks-wall-clock.js').Datetime; +/** + * File size or length of a region within a file. + */ +export type Filesize = bigint; +/** + * The type of a filesystem object referenced by a descriptor. + * + * Note: This was called `filetype` in earlier versions of WASI. + * # Variants + * + * ## `"unknown"` + * + * The type of the descriptor or file is unknown or is different from + * any of the other types specified. + * ## `"block-device"` + * + * The descriptor refers to a block device inode. + * ## `"character-device"` + * + * The descriptor refers to a character device inode. + * ## `"directory"` + * + * The descriptor refers to a directory inode. + * ## `"fifo"` + * + * The descriptor refers to a named pipe. + * ## `"symbolic-link"` + * + * The file refers to a symbolic link inode. + * ## `"regular-file"` + * + * The descriptor refers to a regular file inode. + * ## `"socket"` + * + * The descriptor refers to a socket. + */ +export type DescriptorType = 'unknown' | 'block-device' | 'character-device' | 'directory' | 'fifo' | 'symbolic-link' | 'regular-file' | 'socket'; +/** + * Descriptor flags. + * + * Note: This was called `fdflags` in earlier versions of WASI. + */ +export interface DescriptorFlags { + /** + * Read mode: Data can be read. + */ + read?: boolean, + /** + * Write mode: Data can be written to. + */ + write?: boolean, + /** + * 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. + */ + fileIntegritySync?: boolean, + /** + * 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. + */ + dataIntegritySync?: boolean, + /** + * 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. + */ + requestedWriteSync?: boolean, + /** + * 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. + */ + mutateDirectory?: boolean, +} +/** + * Flags determining the method of how paths are resolved. + */ +export interface PathFlags { + /** + * As long as the resolved path corresponds to a symbolic link, it is + * expanded. + */ + symlinkFollow?: boolean, +} +/** + * Open flags used by `open-at`. + */ +export interface OpenFlags { + /** + * Create file if it does not exist, similar to `O_CREAT` in POSIX. + */ + create?: boolean, + /** + * Fail if not a directory, similar to `O_DIRECTORY` in POSIX. + */ + directory?: boolean, + /** + * Fail if file already exists, similar to `O_EXCL` in POSIX. + */ + exclusive?: boolean, + /** + * Truncate file to size 0, similar to `O_TRUNC` in POSIX. + */ + truncate?: boolean, +} +/** + * Number of hard links to an inode. + */ +export type LinkCount = bigint; +/** + * File attributes. + * + * Note: This was called `filestat` in earlier versions of WASI. + */ +export interface DescriptorStat { + /** + * File type. + */ + type: DescriptorType, + /** + * Number of hard links to the file. + */ + linkCount: LinkCount, + /** + * 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. + */ + dataAccessTimestamp?: Datetime, + /** + * Last data modification timestamp. + * + * If the `option` is none, the platform doesn't maintain a + * modification timestamp for this file. + */ + dataModificationTimestamp?: Datetime, + /** + * Last file status-change timestamp. + * + * If the `option` is none, the platform doesn't maintain a + * status-change timestamp for this file. + */ + statusChangeTimestamp?: Datetime, +} +/** + * When setting a timestamp, this gives the value to set it to. + */ +export type NewTimestamp = NewTimestampNoChange | NewTimestampNow | NewTimestampTimestamp; +/** + * Leave the timestamp set to its previous value. + */ +export interface NewTimestampNoChange { + tag: 'no-change', +} +/** + * Set the timestamp to the current time of the system clock associated + * with the filesystem. + */ +export interface NewTimestampNow { + tag: 'now', +} +/** + * Set the timestamp to the given value. + */ +export interface NewTimestampTimestamp { + tag: 'timestamp', + val: Datetime, +} +/** + * A directory entry. + */ +export interface DirectoryEntry { + /** + * The type of the file referred to by this directory entry. + */ + type: DescriptorType, + /** + * 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. + * # Variants + * + * ## `"access"` + * + * Permission denied, similar to `EACCES` in POSIX. + * ## `"would-block"` + * + * Resource unavailable, or operation would block, similar to `EAGAIN` and `EWOULDBLOCK` in POSIX. + * ## `"already"` + * + * Connection already in progress, similar to `EALREADY` in POSIX. + * ## `"bad-descriptor"` + * + * Bad descriptor, similar to `EBADF` in POSIX. + * ## `"busy"` + * + * Device or resource busy, similar to `EBUSY` in POSIX. + * ## `"deadlock"` + * + * Resource deadlock would occur, similar to `EDEADLK` in POSIX. + * ## `"quota"` + * + * Storage quota exceeded, similar to `EDQUOT` in POSIX. + * ## `"exist"` + * + * File exists, similar to `EEXIST` in POSIX. + * ## `"file-too-large"` + * + * File too large, similar to `EFBIG` in POSIX. + * ## `"illegal-byte-sequence"` + * + * Illegal byte sequence, similar to `EILSEQ` in POSIX. + * ## `"in-progress"` + * + * Operation in progress, similar to `EINPROGRESS` in POSIX. + * ## `"interrupted"` + * + * Interrupted function, similar to `EINTR` in POSIX. + * ## `"invalid"` + * + * Invalid argument, similar to `EINVAL` in POSIX. + * ## `"io"` + * + * I/O error, similar to `EIO` in POSIX. + * ## `"is-directory"` + * + * Is a directory, similar to `EISDIR` in POSIX. + * ## `"loop"` + * + * Too many levels of symbolic links, similar to `ELOOP` in POSIX. + * ## `"too-many-links"` + * + * Too many links, similar to `EMLINK` in POSIX. + * ## `"message-size"` + * + * Message too large, similar to `EMSGSIZE` in POSIX. + * ## `"name-too-long"` + * + * Filename too long, similar to `ENAMETOOLONG` in POSIX. + * ## `"no-device"` + * + * No such device, similar to `ENODEV` in POSIX. + * ## `"no-entry"` + * + * No such file or directory, similar to `ENOENT` in POSIX. + * ## `"no-lock"` + * + * No locks available, similar to `ENOLCK` in POSIX. + * ## `"insufficient-memory"` + * + * Not enough space, similar to `ENOMEM` in POSIX. + * ## `"insufficient-space"` + * + * No space left on device, similar to `ENOSPC` in POSIX. + * ## `"not-directory"` + * + * Not a directory or a symbolic link to a directory, similar to `ENOTDIR` in POSIX. + * ## `"not-empty"` + * + * Directory not empty, similar to `ENOTEMPTY` in POSIX. + * ## `"not-recoverable"` + * + * State not recoverable, similar to `ENOTRECOVERABLE` in POSIX. + * ## `"unsupported"` + * + * Not supported, similar to `ENOTSUP` and `ENOSYS` in POSIX. + * ## `"no-tty"` + * + * Inappropriate I/O control operation, similar to `ENOTTY` in POSIX. + * ## `"no-such-device"` + * + * No such device or address, similar to `ENXIO` in POSIX. + * ## `"overflow"` + * + * Value too large to be stored in data type, similar to `EOVERFLOW` in POSIX. + * ## `"not-permitted"` + * + * Operation not permitted, similar to `EPERM` in POSIX. + * ## `"pipe"` + * + * Broken pipe, similar to `EPIPE` in POSIX. + * ## `"read-only"` + * + * Read-only file system, similar to `EROFS` in POSIX. + * ## `"invalid-seek"` + * + * Invalid seek, similar to `ESPIPE` in POSIX. + * ## `"text-file-busy"` + * + * Text file busy, similar to `ETXTBSY` in POSIX. + * ## `"cross-device"` + * + * Cross-device link, similar to `EXDEV` in POSIX. + */ +export type ErrorCode = 'access' | 'would-block' | 'already' | 'bad-descriptor' | 'busy' | 'deadlock' | 'quota' | 'exist' | 'file-too-large' | 'illegal-byte-sequence' | 'in-progress' | 'interrupted' | 'invalid' | 'io' | 'is-directory' | 'loop' | 'too-many-links' | 'message-size' | 'name-too-long' | 'no-device' | 'no-entry' | 'no-lock' | 'insufficient-memory' | 'insufficient-space' | 'not-directory' | 'not-empty' | 'not-recoverable' | 'unsupported' | 'no-tty' | 'no-such-device' | 'overflow' | 'not-permitted' | 'pipe' | 'read-only' | 'invalid-seek' | 'text-file-busy' | 'cross-device'; +/** + * File or memory access pattern advisory information. + * # Variants + * + * ## `"normal"` + * + * The application has no advice to give on its behavior with respect + * to the specified data. + * ## `"sequential"` + * + * The application expects to access the specified data sequentially + * from lower offsets to higher offsets. + * ## `"random"` + * + * The application expects to access the specified data in a random + * order. + * ## `"will-need"` + * + * The application expects to access the specified data in the near + * future. + * ## `"dont-need"` + * + * The application expects that it will not access the specified data + * in the near future. + * ## `"no-reuse"` + * + * The application expects to access the specified data once and then + * not reuse it thereafter. + */ +export type Advice = 'normal' | 'sequential' | 'random' | 'will-need' | 'dont-need' | 'no-reuse'; +/** + * A 128-bit hash value, split into parts because wasm doesn't have a + * 128-bit integer type. + */ +export interface MetadataHashValue { + /** + * 64 bits of a 128-bit hash value. + */ + lower: bigint, + /** + * Another 64 bits of a 128-bit hash value. + */ + upper: bigint, +} + +export class Descriptor { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Return a stream for reading from a file, if available. + * + * May fail with an error-code describing why the file cannot be read. + * + * Multiple read, write, and append streams may be active on the same open + * file and they do not interfere with each other. + * + * Note: This allows using `read-stream`, which is similar to `read` in POSIX. + */ + readViaStream(offset: Filesize): InputStream; + /** + * Return a stream for writing to a file, if available. + * + * May fail with an error-code describing why the file cannot be written. + * + * Note: This allows using `write-stream`, which is similar to `write` in + * POSIX. + */ + writeViaStream(offset: Filesize): OutputStream; + /** + * Return a stream for appending to a file, if available. + * + * May fail with an error-code describing why the file cannot be appended. + * + * Note: This allows using `write-stream`, which is similar to `write` with + * `O_APPEND` in POSIX. + */ + appendViaStream(): OutputStream; + /** + * Provide file advisory information on a descriptor. + * + * This is similar to `posix_fadvise` in POSIX. + */ + advise(offset: Filesize, length: Filesize, advice: Advice): void; + /** + * 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. + */ + syncData(): void; + /** + * 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. + */ + getFlags(): DescriptorFlags; + /** + * 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. + */ + getType(): DescriptorType; + /** + * 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. + */ + setSize(size: Filesize): void; + /** + * 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. + */ + setTimes(dataAccessTimestamp: NewTimestamp, dataModificationTimestamp: NewTimestamp): void; + /** + * Read from a descriptor, without using and updating the descriptor's offset. + * + * This function returns a list of bytes containing the data that was + * read, along with a bool which, when true, indicates that the end of the + * file was reached. The returned list will contain up to `length` bytes; it + * may return fewer than requested, if the end of the file is reached or + * if the I/O operation is interrupted. + * + * In the future, this may change to return a `stream`. + * + * Note: This is similar to `pread` in POSIX. + */ + read(length: Filesize, offset: Filesize): [Uint8Array, boolean]; + /** + * Write to a descriptor, without using and updating the descriptor's offset. + * + * It is valid to write past the end of a file; the file is extended to the + * extent of the write, with bytes between the previous end and the start of + * the write set to zero. + * + * In the future, this may change to take a `stream`. + * + * Note: This is similar to `pwrite` in POSIX. + */ + write(buffer: Uint8Array, offset: Filesize): Filesize; + /** + * 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. + */ + readDirectory(): DirectoryEntryStream; + /** + * 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. + */ + sync(): void; + /** + * Create a directory. + * + * Note: This is similar to `mkdirat` in POSIX. + */ + createDirectoryAt(path: string): void; + /** + * 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. + */ + stat(): DescriptorStat; + /** + * 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. + */ + statAt(pathFlags: PathFlags, path: string): DescriptorStat; + /** + * 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. + */ + setTimesAt(pathFlags: PathFlags, path: string, dataAccessTimestamp: NewTimestamp, dataModificationTimestamp: NewTimestamp): void; + /** + * Create a hard link. + * + * Note: This is similar to `linkat` in POSIX. + */ + linkAt(oldPathFlags: PathFlags, oldPath: string, newDescriptor: Descriptor, newPath: string): void; + /** + * 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. + */ + openAt(pathFlags: PathFlags, path: string, openFlags: OpenFlags, flags: DescriptorFlags): Descriptor; + /** + * 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. + */ + readlinkAt(path: string): string; + /** + * 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. + */ + removeDirectoryAt(path: string): void; + /** + * Rename a filesystem object. + * + * Note: This is similar to `renameat` in POSIX. + */ + renameAt(oldPath: string, newDescriptor: Descriptor, newPath: string): void; + /** + * 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. + */ + symlinkAt(oldPath: string, newPath: string): void; + /** + * 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. + */ + unlinkFileAt(path: string): void; + /** + * 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. + */ + isSameObject(other: Descriptor): boolean; + /** + * 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. + */ + metadataHash(): MetadataHashValue; + /** + * 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`. + */ + metadataHashAt(pathFlags: PathFlags, path: string): MetadataHashValue; +} + +export class DirectoryEntryStream { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Read a single directory entry from a `directory-entry-stream`. + */ + readDirectoryEntry(): DirectoryEntry | undefined; +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-incoming-handler.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-incoming-handler.d.ts new file mode 100644 index 0000000..3a1ffb1 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-incoming-handler.d.ts @@ -0,0 +1,16 @@ +/** @module Interface wasi:http/incoming-handler@0.2.3 **/ +/** + * This function is invoked with an incoming HTTP Request, and a resource + * `response-outparam` which provides the capability to reply with an HTTP + * Response. The response is sent by calling the `response-outparam.set` + * method, which allows execution to continue after the response has been + * sent. This enables both streaming to the response body, and performing other + * work. + * + * The implementor of this function must write a response to the + * `response-outparam` before returning, or else the caller will respond + * with an error on its behalf. + */ +export function handle(request: IncomingRequest, responseOut: ResponseOutparam): void; +export type IncomingRequest = import('./wasi-http-types.js').IncomingRequest; +export type ResponseOutparam = import('./wasi-http-types.js').ResponseOutparam; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-outgoing-handler.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-outgoing-handler.d.ts new file mode 100644 index 0000000..19566e9 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-outgoing-handler.d.ts @@ -0,0 +1,18 @@ +/** @module Interface wasi:http/outgoing-handler@0.2.3 **/ +/** + * This function is invoked with an outgoing HTTP Request, and it returns + * a resource `future-incoming-response` which represents an HTTP Response + * which may arrive in the future. + * + * The `options` argument accepts optional parameters for the HTTP + * protocol's transport layer. + * + * This function may return an error if the `outgoing-request` is invalid + * or not allowed to be made. Otherwise, protocol errors are reported + * through the `future-incoming-response`. + */ +export function handle(request: OutgoingRequest, options: RequestOptions | undefined): FutureIncomingResponse; +export type OutgoingRequest = import('./wasi-http-types.js').OutgoingRequest; +export type RequestOptions = import('./wasi-http-types.js').RequestOptions; +export type FutureIncomingResponse = import('./wasi-http-types.js').FutureIncomingResponse; +export type ErrorCode = import('./wasi-http-types.js').ErrorCode; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-types.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-types.d.ts new file mode 100644 index 0000000..6116667 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-http-types.d.ts @@ -0,0 +1,748 @@ +/** @module Interface wasi:http/types@0.2.3 **/ +/** + * Attempts to extract a http-related `error` from the wasi:io `error` + * provided. + * + * Stream operations which return + * `wasi:io/stream/stream-error::last-operation-failed` have a payload of + * type `wasi:io/error/error` with more information about the operation + * that failed. This payload can be passed through to this function to see + * if there's http-related information about the error to return. + * + * Note that this function is fallible because not all io-errors are + * http-related errors. + */ +export function httpErrorCode(err: IoError): ErrorCode | undefined; +export type Duration = import('./wasi-clocks-monotonic-clock.js').Duration; +export type InputStream = import('./wasi-io-streams.js').InputStream; +export type OutputStream = import('./wasi-io-streams.js').OutputStream; +export type IoError = import('./wasi-io-error.js').Error; +export type Pollable = import('./wasi-io-poll.js').Pollable; +/** + * This type corresponds to HTTP standard Methods. + */ +export type Method = MethodGet | MethodHead | MethodPost | MethodPut | MethodDelete | MethodConnect | MethodOptions | MethodTrace | MethodPatch | MethodOther; +export interface MethodGet { + tag: 'get', +} +export interface MethodHead { + tag: 'head', +} +export interface MethodPost { + tag: 'post', +} +export interface MethodPut { + tag: 'put', +} +export interface MethodDelete { + tag: 'delete', +} +export interface MethodConnect { + tag: 'connect', +} +export interface MethodOptions { + tag: 'options', +} +export interface MethodTrace { + tag: 'trace', +} +export interface MethodPatch { + tag: 'patch', +} +export interface MethodOther { + tag: 'other', + val: string, +} +/** + * This type corresponds to HTTP standard Related Schemes. + */ +export type Scheme = SchemeHttp | SchemeHttps | SchemeOther; +export interface SchemeHttp { + tag: 'HTTP', +} +export interface SchemeHttps { + tag: 'HTTPS', +} +export interface SchemeOther { + tag: 'other', + val: string, +} +/** + * Defines the case payload type for `DNS-error` above: + */ +export interface DnsErrorPayload { + rcode?: string, + infoCode?: number, +} +/** + * Defines the case payload type for `TLS-alert-received` above: + */ +export interface TlsAlertReceivedPayload { + alertId?: number, + alertMessage?: string, +} +/** + * Defines the case payload type for `HTTP-response-{header,trailer}-size` above: + */ +export interface FieldSizePayload { + fieldName?: string, + fieldSize?: number, +} +/** + * These cases are inspired by the IANA HTTP Proxy Error Types: + * + */ +export type ErrorCode = ErrorCodeDnsTimeout | ErrorCodeDnsError | ErrorCodeDestinationNotFound | ErrorCodeDestinationUnavailable | ErrorCodeDestinationIpProhibited | ErrorCodeDestinationIpUnroutable | ErrorCodeConnectionRefused | ErrorCodeConnectionTerminated | ErrorCodeConnectionTimeout | ErrorCodeConnectionReadTimeout | ErrorCodeConnectionWriteTimeout | ErrorCodeConnectionLimitReached | ErrorCodeTlsProtocolError | ErrorCodeTlsCertificateError | ErrorCodeTlsAlertReceived | ErrorCodeHttpRequestDenied | ErrorCodeHttpRequestLengthRequired | ErrorCodeHttpRequestBodySize | ErrorCodeHttpRequestMethodInvalid | ErrorCodeHttpRequestUriInvalid | ErrorCodeHttpRequestUriTooLong | ErrorCodeHttpRequestHeaderSectionSize | ErrorCodeHttpRequestHeaderSize | ErrorCodeHttpRequestTrailerSectionSize | ErrorCodeHttpRequestTrailerSize | ErrorCodeHttpResponseIncomplete | ErrorCodeHttpResponseHeaderSectionSize | ErrorCodeHttpResponseHeaderSize | ErrorCodeHttpResponseBodySize | ErrorCodeHttpResponseTrailerSectionSize | ErrorCodeHttpResponseTrailerSize | ErrorCodeHttpResponseTransferCoding | ErrorCodeHttpResponseContentCoding | ErrorCodeHttpResponseTimeout | ErrorCodeHttpUpgradeFailed | ErrorCodeHttpProtocolError | ErrorCodeLoopDetected | ErrorCodeConfigurationError | ErrorCodeInternalError; +export interface ErrorCodeDnsTimeout { + tag: 'DNS-timeout', +} +export interface ErrorCodeDnsError { + tag: 'DNS-error', + val: DnsErrorPayload, +} +export interface ErrorCodeDestinationNotFound { + tag: 'destination-not-found', +} +export interface ErrorCodeDestinationUnavailable { + tag: 'destination-unavailable', +} +export interface ErrorCodeDestinationIpProhibited { + tag: 'destination-IP-prohibited', +} +export interface ErrorCodeDestinationIpUnroutable { + tag: 'destination-IP-unroutable', +} +export interface ErrorCodeConnectionRefused { + tag: 'connection-refused', +} +export interface ErrorCodeConnectionTerminated { + tag: 'connection-terminated', +} +export interface ErrorCodeConnectionTimeout { + tag: 'connection-timeout', +} +export interface ErrorCodeConnectionReadTimeout { + tag: 'connection-read-timeout', +} +export interface ErrorCodeConnectionWriteTimeout { + tag: 'connection-write-timeout', +} +export interface ErrorCodeConnectionLimitReached { + tag: 'connection-limit-reached', +} +export interface ErrorCodeTlsProtocolError { + tag: 'TLS-protocol-error', +} +export interface ErrorCodeTlsCertificateError { + tag: 'TLS-certificate-error', +} +export interface ErrorCodeTlsAlertReceived { + tag: 'TLS-alert-received', + val: TlsAlertReceivedPayload, +} +export interface ErrorCodeHttpRequestDenied { + tag: 'HTTP-request-denied', +} +export interface ErrorCodeHttpRequestLengthRequired { + tag: 'HTTP-request-length-required', +} +export interface ErrorCodeHttpRequestBodySize { + tag: 'HTTP-request-body-size', + val: bigint | undefined, +} +export interface ErrorCodeHttpRequestMethodInvalid { + tag: 'HTTP-request-method-invalid', +} +export interface ErrorCodeHttpRequestUriInvalid { + tag: 'HTTP-request-URI-invalid', +} +export interface ErrorCodeHttpRequestUriTooLong { + tag: 'HTTP-request-URI-too-long', +} +export interface ErrorCodeHttpRequestHeaderSectionSize { + tag: 'HTTP-request-header-section-size', + val: number | undefined, +} +export interface ErrorCodeHttpRequestHeaderSize { + tag: 'HTTP-request-header-size', + val: FieldSizePayload | undefined, +} +export interface ErrorCodeHttpRequestTrailerSectionSize { + tag: 'HTTP-request-trailer-section-size', + val: number | undefined, +} +export interface ErrorCodeHttpRequestTrailerSize { + tag: 'HTTP-request-trailer-size', + val: FieldSizePayload, +} +export interface ErrorCodeHttpResponseIncomplete { + tag: 'HTTP-response-incomplete', +} +export interface ErrorCodeHttpResponseHeaderSectionSize { + tag: 'HTTP-response-header-section-size', + val: number | undefined, +} +export interface ErrorCodeHttpResponseHeaderSize { + tag: 'HTTP-response-header-size', + val: FieldSizePayload, +} +export interface ErrorCodeHttpResponseBodySize { + tag: 'HTTP-response-body-size', + val: bigint | undefined, +} +export interface ErrorCodeHttpResponseTrailerSectionSize { + tag: 'HTTP-response-trailer-section-size', + val: number | undefined, +} +export interface ErrorCodeHttpResponseTrailerSize { + tag: 'HTTP-response-trailer-size', + val: FieldSizePayload, +} +export interface ErrorCodeHttpResponseTransferCoding { + tag: 'HTTP-response-transfer-coding', + val: string | undefined, +} +export interface ErrorCodeHttpResponseContentCoding { + tag: 'HTTP-response-content-coding', + val: string | undefined, +} +export interface ErrorCodeHttpResponseTimeout { + tag: 'HTTP-response-timeout', +} +export interface ErrorCodeHttpUpgradeFailed { + tag: 'HTTP-upgrade-failed', +} +export interface ErrorCodeHttpProtocolError { + tag: 'HTTP-protocol-error', +} +export interface ErrorCodeLoopDetected { + tag: 'loop-detected', +} +export interface ErrorCodeConfigurationError { + tag: 'configuration-error', +} +/** + * This is a catch-all error for anything that doesn't fit cleanly into a + * more specific case. It also includes an optional string for an + * unstructured description of the error. Users should not depend on the + * string for diagnosing errors, as it's not required to be consistent + * between implementations. + */ +export interface ErrorCodeInternalError { + tag: 'internal-error', + val: string | undefined, +} +/** + * This type enumerates the different kinds of errors that may occur when + * setting or appending to a `fields` resource. + */ +export type HeaderError = HeaderErrorInvalidSyntax | HeaderErrorForbidden | HeaderErrorImmutable; +/** + * This error indicates that a `field-name` or `field-value` was + * syntactically invalid when used with an operation that sets headers in a + * `fields`. + */ +export interface HeaderErrorInvalidSyntax { + tag: 'invalid-syntax', +} +/** + * This error indicates that a forbidden `field-name` was used when trying + * to set a header in a `fields`. + */ +export interface HeaderErrorForbidden { + tag: 'forbidden', +} +/** + * This error indicates that the operation on the `fields` was not + * permitted because the fields are immutable. + */ +export interface HeaderErrorImmutable { + tag: 'immutable', +} +/** + * Field keys are always strings. + * + * Field keys should always be treated as case insensitive by the `fields` + * resource for the purposes of equality checking. + * + * # Deprecation + * + * This type has been deprecated in favor of the `field-name` type. + */ +export type FieldKey = string; +/** + * Field names are always strings. + * + * Field names should always be treated as case insensitive by the `fields` + * resource for the purposes of equality checking. + */ +export type FieldName = FieldKey; +/** + * 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. + */ +export type FieldValue = Uint8Array; +/** + * Headers is an alias for Fields. + */ +export type Headers = Fields; +/** + * Trailers is an alias for Fields. + */ +export type Trailers = Fields; +/** + * This type corresponds to the HTTP standard Status Code. + */ +export type StatusCode = number; +export type Result = { tag: 'ok', val: T } | { tag: 'err', val: E }; + +export class Fields { + /** + * Construct an empty HTTP Fields. + * + * The resulting `fields` is mutable. + */ + constructor() + /** + * Construct an HTTP Fields. + * + * The resulting `fields` is mutable. + * + * The list represents each name-value pair in the Fields. Names + * which have multiple values are represented by multiple entries in this + * list with the same name. + * + * The tuple is a pair of the field name, represented as a string, and + * Value, represented as a list of bytes. + * + * An error result will be returned if any `field-name` or `field-value` is + * syntactically invalid, or if a field is forbidden. + */ + static fromList(entries: Array<[FieldName, FieldValue]>): Fields; + /** + * Get all of the values corresponding to a name. If the name is not present + * in this `fields` or is syntactically invalid, an empty list is returned. + * However, if the name is present but empty, this is represented by a list + * with one or more empty field-values present. + */ + get(name: FieldName): Array; + /** + * Returns `true` when the name is present in this `fields`. If the name is + * syntactically invalid, `false` is returned. + */ + has(name: FieldName): boolean; + /** + * Set all of the values for a name. Clears any existing values for that + * name, if they have been set. + * + * Fails with `header-error.immutable` if the `fields` are immutable. + * + * Fails with `header-error.invalid-syntax` if the `field-name` or any of + * the `field-value`s are syntactically invalid. + */ + set(name: FieldName, value: Array): void; + /** + * Delete all values for a name. Does nothing if no values for the name + * exist. + * + * Fails with `header-error.immutable` if the `fields` are immutable. + * + * Fails with `header-error.invalid-syntax` if the `field-name` is + * syntactically invalid. + */ + 'delete'(name: FieldName): void; + /** + * Append a value for a name. Does not change or delete any existing + * values for that name. + * + * Fails with `header-error.immutable` if the `fields` are immutable. + * + * Fails with `header-error.invalid-syntax` if the `field-name` or + * `field-value` are syntactically invalid. + */ + append(name: FieldName, value: FieldValue): void; + /** + * Retrieve the full set of names and values in the Fields. Like the + * constructor, the list represents each name-value pair. + * + * The outer list represents each name-value pair in the Fields. Names + * which have multiple values are represented by multiple entries in this + * list with the same name. + * + * The names and values are always returned in the original casing and in + * the order in which they will be serialized for transport. + */ + entries(): 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` is mutable. + */ + clone(): Fields; +} + +export class FutureIncomingResponse { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Returns a pollable which becomes ready when either the Response has + * been received, or an error has occurred. When this pollable is ready, + * the `get` method will return `some`. + */ + subscribe(): Pollable; + /** + * Returns the incoming HTTP Response, or an error, once one is ready. + * + * The outer `option` represents future readiness. Users can wait on this + * `option` to become `some` using the `subscribe` method. + * + * The outer `result` is used to retrieve the response or error at most + * once. It will be success on the first call in which the outer option + * is `some`, and error on subsequent calls. + * + * The inner `result` represents that either the incoming HTTP Response + * status and headers have received successfully, or that an error + * occurred. Errors may also occur while consuming the response body, + * but those will be reported by the `incoming-body` and its + * `output-stream` child. + */ + get(): Result, void> | undefined; +} + +export class FutureTrailers { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Returns a pollable which becomes ready when either the trailers have + * been received, or an error has occurred. When this pollable is ready, + * the `get` method will return `some`. + */ + subscribe(): Pollable; + /** + * Returns the contents of the trailers, or an error which occurred, + * once the future is ready. + * + * The outer `option` represents future readiness. Users can wait on this + * `option` to become `some` using the `subscribe` method. + * + * The outer `result` is used to retrieve the trailers or error at most + * once. It will be success on the first call in which the outer option + * is `some`, and error on subsequent calls. + * + * The inner `result` represents that either the HTTP Request or Response + * body, as well as any trailers, were received successfully, or that an + * error occurred receiving them. The optional `trailers` indicates whether + * or not trailers were present in the body. + * + * When some `trailers` are returned by this method, the `trailers` + * resource is immutable, and a child. Use of the `set`, `append`, or + * `delete` methods will return an error, and the resource must be + * dropped before the parent `future-trailers` is dropped. + */ + get(): Result, void> | undefined; +} + +export class IncomingBody { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Returns the contents of the body, as a stream of bytes. + * + * Returns success on first call: the stream representing the contents + * can be retrieved at most once. Subsequent calls will return error. + * + * The returned `input-stream` resource is a child: it must be dropped + * before the parent `incoming-body` is dropped, or consumed by + * `incoming-body.finish`. + * + * This invariant ensures that the implementation can determine whether + * the user is consuming the contents of the body, waiting on the + * `future-trailers` to be ready, or neither. This allows for network + * backpressure is to be applied when the user is consuming the body, + * and for that backpressure to not inhibit delivery of the trailers if + * the user does not read the entire body. + */ + stream(): InputStream; + /** + * Takes ownership of `incoming-body`, and returns a `future-trailers`. + * This function will trap if the `input-stream` child is still alive. + */ + static finish(this_: IncomingBody): FutureTrailers; +} + +export class IncomingRequest { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Returns the method of the incoming request. + */ + method(): Method; + /** + * Returns the path with query parameters from the request, as a string. + */ + pathWithQuery(): string | undefined; + /** + * Returns the protocol scheme from the request. + */ + scheme(): Scheme | undefined; + /** + * Returns the authority of the Request's target URI, if present. + */ + authority(): string | 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`. + * + * The `headers` returned are a child resource: it must be dropped before + * the parent `incoming-request` is dropped. Dropping this + * `incoming-request` before all children are dropped will trap. + */ + headers(): Headers; + /** + * Gives the `incoming-body` associated with this request. Will only + * return success at most once, and subsequent calls will return error. + */ + consume(): IncomingBody; +} + +export class IncomingResponse { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Returns the status code from the incoming response. + */ + status(): StatusCode; + /** + * Returns the headers from the incoming response. + * + * The returned `headers` resource is immutable: `set`, `append`, and + * `delete` operations will fail with `header-error.immutable`. + * + * This headers resource is a child: it must be dropped before the parent + * `incoming-response` is dropped. + */ + headers(): Headers; + /** + * Returns the incoming body. May be called at most once. Returns error + * if called additional times. + */ + consume(): IncomingBody; +} + +export class OutgoingBody { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Returns a stream for writing the body contents. + * + * The returned `output-stream` is a child resource: it must be dropped + * before the parent `outgoing-body` resource is dropped (or finished), + * otherwise the `outgoing-body` drop or `finish` will trap. + * + * Returns success on the first call: the `output-stream` resource for + * this `outgoing-body` may be retrieved at most once. Subsequent calls + * will return error. + */ + write(): OutputStream; + /** + * Finalize an outgoing body, optionally providing trailers. This must be + * called to signal that the response is complete. If the `outgoing-body` + * is dropped without calling `outgoing-body.finalize`, the implementation + * should treat the body as corrupted. + * + * Fails if the body's `outgoing-request` or `outgoing-response` was + * constructed with a Content-Length header, and the contents written + * to the body (via `write`) does not match the value given in the + * Content-Length. + */ + static finish(this_: OutgoingBody, trailers: Trailers | undefined): void; +} + +export class OutgoingRequest { + /** + * Construct a new `outgoing-request` with a default `method` of `GET`, and + * `none` values for `path-with-query`, `scheme`, and `authority`. + * + * * `headers` is the HTTP Headers for the Request. + * + * It is possible to construct, or manipulate with the accessor functions + * below, an `outgoing-request` with an invalid combination of `scheme` + * and `authority`, or `headers` which are not permitted to be sent. + * It is the obligation of the `outgoing-handler.handle` implementation + * to reject invalid constructions of `outgoing-request`. + */ + constructor(headers: Headers) + /** + * Returns the resource corresponding to the outgoing Body for this + * Request. + * + * Returns success on the first call: the `outgoing-body` resource for + * this `outgoing-request` can be retrieved at most once. Subsequent + * calls will return error. + */ + body(): OutgoingBody; + /** + * Get the Method for the Request. + */ + method(): Method; + /** + * Set the Method for the Request. Fails if the string present in a + * `method.other` argument is not a syntactically valid method. + */ + setMethod(method: Method): void; + /** + * 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; + /** + * 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. + */ + setPathWithQuery(pathWithQuery: string | undefined): void; + /** + * Get the HTTP Related Scheme for the Request. When `none`, the + * implementation may choose an appropriate default scheme. + */ + scheme(): Scheme | undefined; + /** + * 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. + */ + setScheme(scheme: Scheme | undefined): void; + /** + * 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(): 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 + * HTTPS schemes always require an authority. Fails if the string given is + * not a syntactically valid URI authority. + */ + setAuthority(authority: string | undefined): void; + /** + * Get the headers associated with the Request. + * + * The returned `headers` resource is immutable: `set`, `append`, and + * `delete` operations will fail with `header-error.immutable`. + * + * This headers resource is a child: it must be dropped before the parent + * `outgoing-request` is dropped, or its ownership is transferred to + * another component by e.g. `outgoing-handler.handle`. + */ + headers(): Headers; +} + +export class OutgoingResponse { + /** + * Construct an `outgoing-response`, with a default `status-code` of `200`. + * If a different `status-code` is needed, it must be set via the + * `set-status-code` method. + * + * * `headers` is the HTTP Headers for the Response. + */ + constructor(headers: Headers) + /** + * Get the HTTP Status Code for the Response. + */ + statusCode(): StatusCode; + /** + * Set the HTTP Status Code for the Response. Fails if the status-code + * given is not a valid http status code. + */ + setStatusCode(statusCode: StatusCode): void; + /** + * Get the headers associated with the Request. + * + * The returned `headers` resource is immutable: `set`, `append`, and + * `delete` operations will fail with `header-error.immutable`. + * + * This headers resource is a child: it must be dropped before the parent + * `outgoing-request` is dropped, or its ownership is transferred to + * another component by e.g. `outgoing-handler.handle`. + */ + headers(): Headers; + /** + * Returns the resource corresponding to the outgoing Body for this Response. + * + * Returns success on the first call: the `outgoing-body` resource for + * this `outgoing-response` can be retrieved at most once. Subsequent + * calls will return error. + */ + body(): OutgoingBody; +} + +export class RequestOptions { + /** + * Construct a default `request-options` value. + */ + constructor() + /** + * The timeout for the initial connect to the HTTP Server. + */ + connectTimeout(): Duration | undefined; + /** + * Set the timeout for the initial connect to the HTTP Server. An error + * return value indicates that this timeout is not supported. + */ + setConnectTimeout(duration: Duration | undefined): void; + /** + * The timeout for receiving the first byte of the Response body. + */ + firstByteTimeout(): 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. + */ + setFirstByteTimeout(duration: Duration | undefined): void; + /** + * The timeout for receiving subsequent chunks of bytes in the Response + * body stream. + */ + betweenBytesTimeout(): 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 + * supported. + */ + setBetweenBytesTimeout(duration: Duration | undefined): void; +} + +export class ResponseOutparam { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Set the value of the `response-outparam` to either send a response, + * or indicate an error. + * + * This method consumes the `response-outparam` to ensure that it is + * called at most once. If it is never called, the implementation + * will respond with an error. + * + * The user may provide an `error` to `response` to allow the + * implementation determine how to respond with an HTTP error response. + */ + static set(param: ResponseOutparam, response: Result): void; +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-error.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-error.d.ts new file mode 100644 index 0000000..bdd20f7 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-error.d.ts @@ -0,0 +1,18 @@ +/** @module Interface wasi:io/error@0.2.3 **/ + +export class Error { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Returns a string that is suitable to assist humans in debugging + * this error. + * + * WARNING: The returned string should not be consumed mechanically! + * It may change across platforms, hosts, or other implementation + * details. Parsing this string is a major platform-compatibility + * hazard. + */ + toDebugString(): string; +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-poll.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-poll.d.ts new file mode 100644 index 0000000..4b71df6 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-poll.d.ts @@ -0,0 +1,44 @@ +/** @module Interface wasi:io/poll@0.2.3 **/ +/** + * Poll for completion on a set of pollables. + * + * This function takes a list of pollables, which identify I/O sources of + * interest, and waits until one or more of the events is ready for I/O. + * + * The result `list` contains one or more indices of handles in the + * argument list that is ready for I/O. + * + * This function traps if either: + * - the list is empty, or: + * - the list contains more elements than can be indexed with a `u32` value. + * + * A timeout can be implemented by adding a pollable from the + * wasi-clocks API to the list. + * + * This function does not return a `result`; polling in itself does not + * do any I/O so it doesn't fail. If any of the I/O sources identified by + * the pollables has an error, it is indicated by marking the source as + * being ready for I/O. + */ +export function poll(in_: Array): Uint32Array; + +export class Pollable { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Return the readiness of a pollable. This function never blocks. + * + * Returns `true` when the pollable is ready, and `false` otherwise. + */ + ready(): boolean; + /** + * `block` returns immediately if the pollable is ready, and otherwise + * blocks until ready. + * + * This function is equivalent to calling `poll.poll` on a list + * containing only this pollable. + */ + block(): void; +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-streams.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-streams.d.ts new file mode 100644 index 0000000..0da6988 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-io-streams.d.ts @@ -0,0 +1,243 @@ +/** @module Interface wasi:io/streams@0.2.3 **/ +export type Error = import('./wasi-io-error.js').Error; +export type Pollable = import('./wasi-io-poll.js').Pollable; +/** + * An error for input-stream and output-stream operations. + */ +export type StreamError = StreamErrorLastOperationFailed | StreamErrorClosed; +/** + * The last operation (a write or flush) failed before completion. + * + * More information is available in the `error` payload. + * + * After this, the stream will be closed. All future operations return + * `stream-error::closed`. + */ +export interface StreamErrorLastOperationFailed { + tag: 'last-operation-failed', + val: Error, +} +/** + * The stream is closed: no more input will be accepted by the + * stream. A closed output-stream will return this error on all + * future operations. + */ +export interface StreamErrorClosed { + tag: 'closed', +} + +export class InputStream { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Perform a non-blocking read from the stream. + * + * When the source of a `read` is binary data, the bytes from the source + * are returned verbatim. When the source of a `read` is known to the + * implementation to be text, bytes containing the UTF-8 encoding of the + * text are returned. + * + * This function returns a list of bytes containing the read data, + * when successful. The returned list will contain up to `len` bytes; + * it may return fewer than requested, but not more. The list is + * empty when no bytes are available for reading at this time. The + * pollable given by `subscribe` will be ready when more bytes are + * available. + * + * This function fails with a `stream-error` when the operation + * encounters an error, giving `last-operation-failed`, or when the + * stream is closed, giving `closed`. + * + * When the caller gives a `len` of 0, it represents a request to + * read 0 bytes. If the stream is still open, this call should + * succeed and return an empty list, or otherwise fail with `closed`. + * + * The `len` parameter is a `u64`, which could represent a list of u8 which + * is not possible to allocate in wasm32, or not desirable to allocate as + * as a return value by the callee. The callee may return a list of bytes + * less than `len` in size while more bytes are available for reading. + */ + read(len: bigint): Uint8Array; + /** + * Read bytes from a stream, after blocking until at least one byte can + * be read. Except for blocking, behavior is identical to `read`. + */ + blockingRead(len: bigint): Uint8Array; + /** + * Skip bytes from a stream. Returns number of bytes skipped. + * + * Behaves identical to `read`, except instead of returning a list + * of bytes, returns the number of bytes consumed from the stream. + */ + skip(len: bigint): bigint; + /** + * Skip bytes from a stream, after blocking until at least one byte + * can be skipped. Except for blocking behavior, identical to `skip`. + */ + blockingSkip(len: bigint): bigint; + /** + * Create a `pollable` which will resolve once either the specified stream + * has bytes available to read or the other end of the stream has been + * closed. + * The created `pollable` is a child resource of the `input-stream`. + * Implementations may trap if the `input-stream` is dropped before + * all derived `pollable`s created with this function are dropped. + */ + subscribe(): Pollable; +} + +export class OutputStream { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Check readiness for writing. This function never blocks. + * + * Returns the number of bytes permitted for the next call to `write`, + * or an error. Calling `write` with more bytes than this function has + * permitted will trap. + * + * When this function returns 0 bytes, the `subscribe` pollable will + * become ready when this function will report at least 1 byte, or an + * error. + */ + checkWrite(): bigint; + /** + * Perform a write. This function never blocks. + * + * When the destination of a `write` is binary data, the bytes from + * `contents` are written verbatim. When the destination of a `write` is + * known to the implementation to be text, the bytes of `contents` are + * transcoded from UTF-8 into the encoding of the destination and then + * written. + * + * Precondition: check-write gave permit of Ok(n) and contents has a + * length of less than or equal to n. Otherwise, this function will trap. + * + * returns Err(closed) without writing if the stream has closed since + * the last call to check-write provided a permit. + */ + write(contents: Uint8Array): void; + /** + * Perform a write of up to 4096 bytes, and then flush the stream. Block + * until all of these operations are complete, or an error occurs. + * + * This is a convenience wrapper around the use of `check-write`, + * `subscribe`, `write`, and `flush`, and is implemented with the + * following pseudo-code: + * + * ```text + * let pollable = this.subscribe(); + * while !contents.is_empty() { + * // Wait for the stream to become writable + * pollable.block(); + * let Ok(n) = this.check-write(); // eliding error handling + * let len = min(n, contents.len()); + * let (chunk, rest) = contents.split_at(len); + * this.write(chunk ); // eliding error handling + * contents = rest; + * } + * this.flush(); + * // Wait for completion of `flush` + * pollable.block(); + * // Check for any errors that arose during `flush` + * let _ = this.check-write(); // eliding error handling + * ``` + */ + blockingWriteAndFlush(contents: Uint8Array): void; + /** + * Request to flush buffered output. This function never blocks. + * + * This tells the output-stream that the caller intends any buffered + * output to be flushed. the output which is expected to be flushed + * is all that has been passed to `write` prior to this call. + * + * Upon calling this function, the `output-stream` will not accept any + * writes (`check-write` will return `ok(0)`) until the flush has + * completed. The `subscribe` pollable will become ready when the + * flush has completed and the stream can accept more writes. + */ + flush(): void; + /** + * Request to flush buffered output, and block until flush completes + * and stream is ready for writing again. + */ + blockingFlush(): void; + /** + * Create a `pollable` which will resolve once the output-stream + * is ready for more writing, or an error has occurred. When this + * pollable is ready, `check-write` will return `ok(n)` with n>0, or an + * error. + * + * If the stream is closed, this pollable is always ready immediately. + * + * The created `pollable` is a child resource of the `output-stream`. + * Implementations may trap if the `output-stream` is dropped before + * all derived `pollable`s created with this function are dropped. + */ + subscribe(): Pollable; + /** + * Write zeroes to a stream. + * + * This should be used precisely like `write` with the exact same + * preconditions (must use check-write first), but instead of + * passing a list of bytes, you simply pass the number of zero-bytes + * that should be written. + */ + writeZeroes(len: bigint): void; + /** + * Perform a write of up to 4096 zeroes, and then flush the stream. + * Block until all of these operations are complete, or an error + * occurs. + * + * This is a convenience wrapper around the use of `check-write`, + * `subscribe`, `write-zeroes`, and `flush`, and is implemented with + * the following pseudo-code: + * + * ```text + * let pollable = this.subscribe(); + * while num_zeroes != 0 { + * // Wait for the stream to become writable + * pollable.block(); + * let Ok(n) = this.check-write(); // eliding error handling + * let len = min(n, num_zeroes); + * this.write-zeroes(len); // eliding error handling + * num_zeroes -= len; + * } + * this.flush(); + * // Wait for completion of `flush` + * pollable.block(); + * // Check for any errors that arose during `flush` + * let _ = this.check-write(); // eliding error handling + * ``` + */ + blockingWriteZeroesAndFlush(len: bigint): void; + /** + * Read from one stream and write to another. + * + * The behavior of splice is equivalent to: + * 1. calling `check-write` on the `output-stream` + * 2. calling `read` on the `input-stream` with the smaller of the + * `check-write` permitted length and the `len` provided to `splice` + * 3. calling `write` on the `output-stream` with that read data. + * + * Any error reported by the call to `check-write`, `read`, or + * `write` ends the splice and reports that error. + * + * This function returns the number of bytes transferred; it may be less + * than `len`. + */ + splice(src: InputStream, len: bigint): bigint; + /** + * Read from one stream and write to another, with blocking. + * + * This is similar to `splice`, except that it blocks until the + * `output-stream` is ready for writing, and the `input-stream` + * is ready for reading, before performing the `splice`. + */ + blockingSplice(src: InputStream, len: bigint): bigint; + } + \ No newline at end of file diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-insecure-seed.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-insecure-seed.d.ts new file mode 100644 index 0000000..2a234cf --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-insecure-seed.d.ts @@ -0,0 +1,21 @@ +/** @module Interface wasi:random/insecure-seed@0.2.3 **/ +/** + * 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. + */ +export function insecureSeed(): [bigint, bigint]; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-insecure.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-insecure.d.ts new file mode 100644 index 0000000..0af5f36 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-insecure.d.ts @@ -0,0 +1,19 @@ +/** @module Interface wasi:random/insecure@0.2.3 **/ +/** + * 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. + */ +export function getInsecureRandomBytes(len: bigint): Uint8Array; +/** + * 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`. + */ +export function getInsecureRandomU64(): bigint; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-random.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-random.d.ts new file mode 100644 index 0000000..d19fc02 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-random-random.d.ts @@ -0,0 +1,23 @@ +/** @module Interface wasi:random/random@0.2.3 **/ +/** + * 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. + */ +export function getRandomBytes(len: bigint): Uint8Array; +/** + * 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`. + */ +export function getRandomU64(): bigint; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-instance-network.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-instance-network.d.ts new file mode 100644 index 0000000..22eea52 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-instance-network.d.ts @@ -0,0 +1,6 @@ +/** @module Interface wasi:sockets/instance-network@0.2.3 **/ +/** + * Get a handle to the default network. + */ +export function instanceNetwork(): Network; +export type Network = import('./wasi-sockets-network.js').Network; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-ip-name-lookup.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-ip-name-lookup.d.ts new file mode 100644 index 0000000..7857d1a --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-ip-name-lookup.d.ts @@ -0,0 +1,58 @@ +/** @module Interface wasi:sockets/ip-name-lookup@0.2.3 **/ +/** + * Resolve an internet host name to a list of IP addresses. + * + * Unicode domain names are automatically converted to ASCII using IDNA encoding. + * If the input is an IP address string, the address is parsed and returned + * as-is without making any external requests. + * + * See the wasi-socket proposal README.md for a comparison with getaddrinfo. + * + * This function never blocks. It either immediately fails or immediately + * returns successfully with a `resolve-address-stream` that can be used + * to (asynchronously) fetch the results. + * + * # Typical errors + * - `invalid-argument`: `name` is a syntactically invalid domain name or IP address. + * + * # References: + * - + * - + * - + * - + */ +export function resolveAddresses(network: Network, name: string): ResolveAddressStream; +export type Pollable = import('./wasi-io-poll.js').Pollable; +export type Network = import('./wasi-sockets-network.js').Network; +export type ErrorCode = import('./wasi-sockets-network.js').ErrorCode; +export type IpAddress = import('./wasi-sockets-network.js').IpAddress; + +export class ResolveAddressStream { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Returns the next address from the resolver. + * + * This function should be called multiple times. On each call, it will + * return the next address in connection order preference. If all + * addresses have been exhausted, this function returns `none`. + * + * This function never returns IPv4-mapped IPv6 addresses. + * + * # Typical errors + * - `name-unresolvable`: Name does not exist or has no suitable associated IP addresses. (EAI_NONAME, EAI_NODATA, EAI_ADDRFAMILY) + * - `temporary-resolver-failure`: A temporary failure in name resolution occurred. (EAI_AGAIN) + * - `permanent-resolver-failure`: A permanent failure in name resolution occurred. (EAI_FAIL) + * - `would-block`: A result is not available yet. (EWOULDBLOCK, EAGAIN) + */ + resolveNextAddress(): IpAddress | undefined; + /** + * Create a `pollable` which will resolve once the stream is ready for I/O. + * + * Note: this function is here for WASI 0.2 only. + * It's planned to be removed when `future` is natively supported in Preview3. + */ + subscribe(): Pollable; +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-network.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-network.d.ts new file mode 100644 index 0000000..be1cf60 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-network.d.ts @@ -0,0 +1,164 @@ +/** @module Interface wasi:sockets/network@0.2.3 **/ +/** + * Error codes. + * + * In theory, every API can return any error code. + * In practice, API's typically only return the errors documented per API + * combined with a couple of errors that are always possible: + * - `unknown` + * - `access-denied` + * - `not-supported` + * - `out-of-memory` + * - `concurrency-conflict` + * + * See each individual API for what the POSIX equivalents are. They sometimes differ per API. + * # Variants + * + * ## `"unknown"` + * + * Unknown error + * ## `"access-denied"` + * + * Access denied. + * + * POSIX equivalent: EACCES, EPERM + * ## `"not-supported"` + * + * The operation is not supported. + * + * POSIX equivalent: EOPNOTSUPP + * ## `"invalid-argument"` + * + * One of the arguments is invalid. + * + * POSIX equivalent: EINVAL + * ## `"out-of-memory"` + * + * Not enough memory to complete the operation. + * + * POSIX equivalent: ENOMEM, ENOBUFS, EAI_MEMORY + * ## `"timeout"` + * + * The operation timed out before it could finish completely. + * ## `"concurrency-conflict"` + * + * This operation is incompatible with another asynchronous operation that is already in progress. + * + * POSIX equivalent: EALREADY + * ## `"not-in-progress"` + * + * Trying to finish an asynchronous operation that: + * - has not been started yet, or: + * - was already finished by a previous `finish-*` call. + * + * Note: this is scheduled to be removed when `future`s are natively supported. + * ## `"would-block"` + * + * The operation has been aborted because it could not be completed immediately. + * + * Note: this is scheduled to be removed when `future`s are natively supported. + * ## `"invalid-state"` + * + * The operation is not valid in the socket's current state. + * ## `"new-socket-limit"` + * + * A new socket resource could not be created because of a system limit. + * ## `"address-not-bindable"` + * + * A bind operation failed because the provided address is not an address that the `network` can bind to. + * ## `"address-in-use"` + * + * A bind operation failed because the provided address is already in use or because there are no ephemeral ports available. + * ## `"remote-unreachable"` + * + * The remote address is not reachable + * ## `"connection-refused"` + * + * The TCP connection was forcefully rejected + * ## `"connection-reset"` + * + * The TCP connection was reset. + * ## `"connection-aborted"` + * + * A TCP connection was aborted. + * ## `"datagram-too-large"` + * + * The size of a datagram sent to a UDP socket exceeded the maximum + * supported size. + * ## `"name-unresolvable"` + * + * Name does not exist or has no suitable associated IP addresses. + * ## `"temporary-resolver-failure"` + * + * A temporary failure in name resolution occurred. + * ## `"permanent-resolver-failure"` + * + * A permanent failure in name resolution occurred. + */ +export type ErrorCode = 'unknown' | 'access-denied' | 'not-supported' | 'invalid-argument' | 'out-of-memory' | 'timeout' | 'concurrency-conflict' | 'not-in-progress' | 'would-block' | 'invalid-state' | 'new-socket-limit' | 'address-not-bindable' | 'address-in-use' | 'remote-unreachable' | 'connection-refused' | 'connection-reset' | 'connection-aborted' | 'datagram-too-large' | 'name-unresolvable' | 'temporary-resolver-failure' | 'permanent-resolver-failure'; +/** + * # Variants + * + * ## `"ipv4"` + * + * Similar to `AF_INET` in POSIX. + * ## `"ipv6"` + * + * Similar to `AF_INET6` in POSIX. + */ +export type IpAddressFamily = 'ipv4' | 'ipv6'; +export type Ipv4Address = [number, number, number, number]; +export type Ipv6Address = [number, number, number, number, number, number, number, number]; +export type IpAddress = IpAddressIpv4 | IpAddressIpv6; +export interface IpAddressIpv4 { + tag: 'ipv4', + val: Ipv4Address, +} +export interface IpAddressIpv6 { + tag: 'ipv6', + val: Ipv6Address, +} +export interface Ipv4SocketAddress { + /** + * sin_port + */ + port: number, + /** + * sin_addr + */ + address: Ipv4Address, +} +export interface Ipv6SocketAddress { + /** + * sin6_port + */ + port: number, + /** + * sin6_flowinfo + */ + flowInfo: number, + /** + * sin6_addr + */ + address: Ipv6Address, + /** + * sin6_scope_id + */ + scopeId: number, +} +export type IpSocketAddress = IpSocketAddressIpv4 | IpSocketAddressIpv6; +export interface IpSocketAddressIpv4 { + tag: 'ipv4', + val: Ipv4SocketAddress, +} +export interface IpSocketAddressIpv6 { + tag: 'ipv6', + val: Ipv6SocketAddress, +} + +export class Network { + /** + * This type does not have a public constructor. + */ + private constructor(); +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-tcp-create-socket.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-tcp-create-socket.d.ts new file mode 100644 index 0000000..73c11ab --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-tcp-create-socket.d.ts @@ -0,0 +1,28 @@ +/** @module Interface wasi:sockets/tcp-create-socket@0.2.3 **/ +/** + * Create a new TCP socket. + * + * Similar to `socket(AF_INET or AF_INET6, SOCK_STREAM, IPPROTO_TCP)` in POSIX. + * On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + * + * This function does not require a network capability handle. This is considered to be safe because + * at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind`/`connect` + * is called, the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + * + * All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + * + * # Typical errors + * - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + * - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + * + * # References + * - + * - + * - + * - + */ +export function createTcpSocket(addressFamily: IpAddressFamily): TcpSocket; +export type Network = import('./wasi-sockets-network.js').Network; +export type ErrorCode = import('./wasi-sockets-network.js').ErrorCode; +export type IpAddressFamily = import('./wasi-sockets-network.js').IpAddressFamily; +export type TcpSocket = import('./wasi-sockets-tcp.js').TcpSocket; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-tcp.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-tcp.d.ts new file mode 100644 index 0000000..7327807 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-tcp.d.ts @@ -0,0 +1,359 @@ +/** @module Interface wasi:sockets/tcp@0.2.3 **/ +export type InputStream = import('./wasi-io-streams.js').InputStream; +export type OutputStream = import('./wasi-io-streams.js').OutputStream; +export type Pollable = import('./wasi-io-poll.js').Pollable; +export type Duration = import('./wasi-clocks-monotonic-clock.js').Duration; +export type Network = import('./wasi-sockets-network.js').Network; +export type ErrorCode = import('./wasi-sockets-network.js').ErrorCode; +export type IpSocketAddress = import('./wasi-sockets-network.js').IpSocketAddress; +export type IpAddressFamily = import('./wasi-sockets-network.js').IpAddressFamily; +/** + * # Variants + * + * ## `"receive"` + * + * Similar to `SHUT_RD` in POSIX. + * ## `"send"` + * + * Similar to `SHUT_WR` in POSIX. + * ## `"both"` + * + * Similar to `SHUT_RDWR` in POSIX. + */ +export type ShutdownType = 'receive' | 'send' | 'both'; + +export class TcpSocket { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Bind the socket to a specific network on the provided IP address and port. + * + * If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + * network interface(s) to bind to. + * If the TCP/UDP port is zero, the socket will be bound to a random free port. + * + * Bind can be attempted multiple times on the same socket, even with + * different arguments on each iteration. But never concurrently and + * only as long as the previous bind failed. Once a bind succeeds, the + * binding can't be changed anymore. + * + * # Typical errors + * - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + * - `invalid-argument`: `local-address` is not a unicast address. (EINVAL) + * - `invalid-argument`: `local-address` is an IPv4-mapped IPv6 address. (EINVAL) + * - `invalid-state`: The socket is already bound. (EINVAL) + * - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + * - `address-in-use`: Address is already in use. (EADDRINUSE) + * - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + * - `not-in-progress`: A `bind` operation is not in progress. + * - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + * + * # Implementors note + * When binding to a non-zero port, this bind operation shouldn't be affected by the TIME_WAIT + * state of a recently closed socket on the same local address. In practice this means that the SO_REUSEADDR + * socket option should be set implicitly on all platforms, except on Windows where this is the default behavior + * and SO_REUSEADDR performs something different entirely. + * + * Unlike in POSIX, in WASI the bind operation is async. This enables + * interactive WASI hosts to inject permission prompts. Runtimes that + * don't want to make use of this ability can simply call the native + * `bind` as part of either `start-bind` or `finish-bind`. + * + * # References + * - + * - + * - + * - + */ + startBind(network: Network, localAddress: IpSocketAddress): void; + finishBind(): void; + /** + * Connect to a remote endpoint. + * + * On success: + * - the socket is transitioned into the `connected` state. + * - a pair of streams is returned that can be used to read & write to the connection + * + * After a failed connection attempt, the socket will be in the `closed` + * state and the only valid action left is to `drop` the socket. A single + * socket can not be used to connect more than once. + * + * # Typical errors + * - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * - `invalid-argument`: `remote-address` is not a unicast address. (EINVAL, ENETUNREACH on Linux, EAFNOSUPPORT on MacOS) + * - `invalid-argument`: `remote-address` is an IPv4-mapped IPv6 address. (EINVAL, EADDRNOTAVAIL on Illumos) + * - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EADDRNOTAVAIL on Windows) + * - `invalid-argument`: The port in `remote-address` is set to 0. (EADDRNOTAVAIL on Windows) + * - `invalid-argument`: The socket is already attached to a different network. The `network` passed to `connect` must be identical to the one passed to `bind`. + * - `invalid-state`: The socket is already in the `connected` state. (EISCONN) + * - `invalid-state`: The socket is already in the `listening` state. (EOPNOTSUPP, EINVAL on Windows) + * - `timeout`: Connection timed out. (ETIMEDOUT) + * - `connection-refused`: The connection was forcefully rejected. (ECONNREFUSED) + * - `connection-reset`: The connection was reset. (ECONNRESET) + * - `connection-aborted`: The connection was aborted. (ECONNABORTED) + * - `remote-unreachable`: The remote address is not reachable. (EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + * - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + * - `not-in-progress`: A connect operation is not in progress. + * - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + * + * # Implementors note + * The POSIX equivalent of `start-connect` is the regular `connect` syscall. + * Because all WASI sockets are non-blocking this is expected to return + * EINPROGRESS, which should be translated to `ok()` in WASI. + * + * The POSIX equivalent of `finish-connect` is a `poll` for event `POLLOUT` + * with a timeout of 0 on the socket descriptor. Followed by a check for + * the `SO_ERROR` socket option, in case the poll signaled readiness. + * + * # References + * - + * - + * - + * - + */ + startConnect(network: Network, remoteAddress: IpSocketAddress): void; + finishConnect(): [InputStream, OutputStream]; + /** + * Start listening for new connections. + * + * Transitions the socket into the `listening` state. + * + * Unlike POSIX, the socket must already be explicitly bound. + * + * # Typical errors + * - `invalid-state`: The socket is not bound to any local address. (EDESTADDRREQ) + * - `invalid-state`: The socket is already in the `connected` state. (EISCONN, EINVAL on BSD) + * - `invalid-state`: The socket is already in the `listening` state. + * - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE) + * - `not-in-progress`: A listen operation is not in progress. + * - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + * + * # Implementors note + * Unlike in POSIX, in WASI the listen operation is async. This enables + * interactive WASI hosts to inject permission prompts. Runtimes that + * don't want to make use of this ability can simply call the native + * `listen` as part of either `start-listen` or `finish-listen`. + * + * # References + * - + * - + * - + * - + */ + startListen(): void; + finishListen(): void; + /** + * Accept a new client socket. + * + * The returned socket is bound and in the `connected` state. The following properties are inherited from the listener socket: + * - `address-family` + * - `keep-alive-enabled` + * - `keep-alive-idle-time` + * - `keep-alive-interval` + * - `keep-alive-count` + * - `hop-limit` + * - `receive-buffer-size` + * - `send-buffer-size` + * + * On success, this function returns the newly accepted client socket along with + * a pair of streams that can be used to read & write to the connection. + * + * # Typical errors + * - `invalid-state`: Socket is not in the `listening` state. (EINVAL) + * - `would-block`: No pending connections at the moment. (EWOULDBLOCK, EAGAIN) + * - `connection-aborted`: An incoming connection was pending, but was terminated by the client before this listener could accept it. (ECONNABORTED) + * - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + * + * # References + * - + * - + * - + * - + */ + accept(): [TcpSocket, InputStream, OutputStream]; + /** + * 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 + * - + * - + * - + * - + */ + localAddress(): IpSocketAddress; + /** + * Get the remote address. + * + * # Typical errors + * - `invalid-state`: The socket is not connected to a remote address. (ENOTCONN) + * + * # References + * - + * - + * - + * - + */ + remoteAddress(): IpSocketAddress; + /** + * Whether the socket is in the `listening` state. + * + * Equivalent to the SO_ACCEPTCONN socket option. + */ + isListening(): boolean; + /** + * Whether this is a IPv4 or IPv6 socket. + * + * Equivalent to the SO_DOMAIN socket option. + */ + addressFamily(): IpAddressFamily; + /** + * Hints the desired listen queue size. Implementations are free to ignore this. + * + * If the provided value is 0, an `invalid-argument` error is returned. + * Any other value will never cause an error, but it might be silently clamped and/or rounded. + * + * # Typical errors + * - `not-supported`: (set) The platform does not support changing the backlog size after the initial listen. + * - `invalid-argument`: (set) The provided value was 0. + * - `invalid-state`: (set) The socket is in the `connect-in-progress` or `connected` state. + */ + setListenBacklogSize(value: bigint): void; + /** + * 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. + */ + keepAliveEnabled(): boolean; + setKeepAliveEnabled(value: boolean): void; + /** + * 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. + */ + keepAliveIdleTime(): Duration; + setKeepAliveIdleTime(value: Duration): void; + /** + * 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. + */ + keepAliveInterval(): Duration; + setKeepAliveInterval(value: Duration): void; + /** + * 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. + */ + keepAliveCount(): number; + setKeepAliveCount(value: number): void; + /** + * 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. + */ + hopLimit(): number; + setHopLimit(value: number): void; + /** + * 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. + */ + receiveBufferSize(): bigint; + setReceiveBufferSize(value: bigint): void; + sendBufferSize(): bigint; + setSendBufferSize(value: bigint): void; + /** + * Create a `pollable` which can be used to poll for, or block on, + * completion of any of the asynchronous operations of this socket. + * + * When `finish-bind`, `finish-listen`, `finish-connect` or `accept` + * return `error(would-block)`, this pollable can be used to wait for + * their success or failure, after which the method can be retried. + * + * The pollable is not limited to the async operation that happens to be + * in progress at the time of calling `subscribe` (if any). Theoretically, + * `subscribe` only has to be called once per socket and can then be + * (re)used for the remainder of the socket's lifetime. + * + * See + * for more information. + * + * Note: this function is here for WASI 0.2 only. + * It's planned to be removed when `future` is natively supported in Preview3. + */ + subscribe(): Pollable; + /** + * Initiate a graceful shutdown. + * + * - `receive`: The socket is not expecting to receive any data from + * the peer. The `input-stream` associated with this socket will be + * closed. Any data still in the receive queue at time of calling + * this method will be discarded. + * - `send`: The socket has no more data to send to the peer. The `output-stream` + * associated with this socket will be closed and a FIN packet will be sent. + * - `both`: Same effect as `receive` & `send` combined. + * + * This function is idempotent; shutting down a direction more than once + * has no effect and returns `ok`. + * + * The shutdown function does not close (drop) the socket. + * + * # Typical errors + * - `invalid-state`: The socket is not in the `connected` state. (ENOTCONN) + * + * # References + * - + * - + * - + * - + */ + shutdown(shutdownType: ShutdownType): void; +} diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-udp-create-socket.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-udp-create-socket.d.ts new file mode 100644 index 0000000..95d8ff7 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-udp-create-socket.d.ts @@ -0,0 +1,28 @@ +/** @module Interface wasi:sockets/udp-create-socket@0.2.3 **/ +/** + * Create a new UDP socket. + * + * Similar to `socket(AF_INET or AF_INET6, SOCK_DGRAM, IPPROTO_UDP)` in POSIX. + * On IPv6 sockets, IPV6_V6ONLY is enabled by default and can't be configured otherwise. + * + * This function does not require a network capability handle. This is considered to be safe because + * at time of creation, the socket is not bound to any `network` yet. Up to the moment `bind` is called, + * the socket is effectively an in-memory configuration object, unable to communicate with the outside world. + * + * All sockets are non-blocking. Use the wasi-poll interface to block on asynchronous operations. + * + * # Typical errors + * - `not-supported`: The specified `address-family` is not supported. (EAFNOSUPPORT) + * - `new-socket-limit`: The new socket resource could not be created because of a system limit. (EMFILE, ENFILE) + * + * # References: + * - + * - + * - + * - + */ +export function createUdpSocket(addressFamily: IpAddressFamily): UdpSocket; +export type Network = import('./wasi-sockets-network.js').Network; +export type ErrorCode = import('./wasi-sockets-network.js').ErrorCode; +export type IpAddressFamily = import('./wasi-sockets-network.js').IpAddressFamily; +export type UdpSocket = import('./wasi-sockets-udp.js').UdpSocket; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-udp.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-udp.d.ts new file mode 100644 index 0000000..dc8ae48 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/interfaces/wasi-sockets-udp.d.ts @@ -0,0 +1,301 @@ +/** @module Interface wasi:sockets/udp@0.2.3 **/ +export type Pollable = import('./wasi-io-poll.js').Pollable; +export type Network = import('./wasi-sockets-network.js').Network; +export type ErrorCode = import('./wasi-sockets-network.js').ErrorCode; +export type IpSocketAddress = import('./wasi-sockets-network.js').IpSocketAddress; +export type IpAddressFamily = import('./wasi-sockets-network.js').IpAddressFamily; +/** + * A received datagram. + */ +export interface IncomingDatagram { + /** + * The payload. + * + * Theoretical max size: ~64 KiB. In practice, typically less than 1500 bytes. + */ + data: Uint8Array, + /** + * The source address. + * + * This field is guaranteed to match the remote address the stream was initialized with, if any. + * + * Equivalent to the `src_addr` out parameter of `recvfrom`. + */ + remoteAddress: IpSocketAddress, +} +/** + * A datagram to be sent out. + */ +export interface OutgoingDatagram { + /** + * The payload. + */ + data: Uint8Array, + /** + * The destination address. + * + * The requirements on this field depend on how the stream was initialized: + * - with a remote address: this field must be None or match the stream's remote address exactly. + * - without a remote address: this field is required. + * + * If this value is None, the send operation is equivalent to `send` in POSIX. Otherwise it is equivalent to `sendto`. + */ + remoteAddress?: IpSocketAddress, +} + +export class IncomingDatagramStream { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Receive messages on the socket. + * + * This function attempts to receive up to `max-results` datagrams on the socket without blocking. + * The returned list may contain fewer elements than requested, but never more. + * + * This function returns successfully with an empty list when either: + * - `max-results` is 0, or: + * - `max-results` is greater than 0, but no results are immediately available. + * This function never returns `error(would-block)`. + * + * # Typical errors + * - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + * - `connection-refused`: The connection was refused. (ECONNREFUSED) + * + * # References + * - + * - + * - + * - + * - + * - + * - + * - + */ + receive(maxResults: bigint): Array; + /** + * Create a `pollable` which will resolve once the stream is ready to receive again. + * + * Note: this function is here for WASI 0.2 only. + * It's planned to be removed when `future` is natively supported in Preview3. + */ + subscribe(): Pollable; +} + +export class OutgoingDatagramStream { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Check readiness for sending. This function never blocks. + * + * Returns the number of datagrams permitted for the next call to `send`, + * or an error. Calling `send` with more datagrams than this function has + * permitted will trap. + * + * When this function returns ok(0), the `subscribe` pollable will + * become ready when this function will report at least ok(1), or an + * error. + * + * Never returns `would-block`. + */ + checkSend(): bigint; + /** + * Send messages on the socket. + * + * This function attempts to send all provided `datagrams` on the socket without blocking and + * returns how many messages were actually sent (or queued for sending). This function never + * returns `error(would-block)`. If none of the datagrams were able to be sent, `ok(0)` is returned. + * + * This function semantically behaves the same as iterating the `datagrams` list and sequentially + * sending each individual datagram until either the end of the list has been reached or the first error occurred. + * If at least one datagram has been sent successfully, this function never returns an error. + * + * If the input list is empty, the function returns `ok(0)`. + * + * Each call to `send` must be permitted by a preceding `check-send`. Implementations must trap if + * either `check-send` was not called or `datagrams` contains more items than `check-send` permitted. + * + * # Typical errors + * - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + * - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + * - `invalid-argument`: The socket is in "connected" mode and `remote-address` is `some` value that does not match the address passed to `stream`. (EISCONN) + * - `invalid-argument`: The socket is not "connected" and no value for `remote-address` was provided. (EDESTADDRREQ) + * - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET on Windows, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + * - `connection-refused`: The connection was refused. (ECONNREFUSED) + * - `datagram-too-large`: The datagram is too large. (EMSGSIZE) + * + * # References + * - + * - + * - + * - + * - + * - + * - + * - + */ + send(datagrams: Array): bigint; + /** + * Create a `pollable` which will resolve once the stream is ready to send again. + * + * Note: this function is here for WASI 0.2 only. + * It's planned to be removed when `future` is natively supported in Preview3. + */ + subscribe(): Pollable; +} + +export class UdpSocket { + /** + * This type does not have a public constructor. + */ + private constructor(); + /** + * Bind the socket to a specific network on the provided IP address and port. + * + * If the IP address is zero (`0.0.0.0` in IPv4, `::` in IPv6), it is left to the implementation to decide which + * network interface(s) to bind to. + * If the port is zero, the socket will be bound to a random free port. + * + * # Typical errors + * - `invalid-argument`: The `local-address` has the wrong address family. (EAFNOSUPPORT, EFAULT on Windows) + * - `invalid-state`: The socket is already bound. (EINVAL) + * - `address-in-use`: No ephemeral ports available. (EADDRINUSE, ENOBUFS on Windows) + * - `address-in-use`: Address is already in use. (EADDRINUSE) + * - `address-not-bindable`: `local-address` is not an address that the `network` can bind to. (EADDRNOTAVAIL) + * - `not-in-progress`: A `bind` operation is not in progress. + * - `would-block`: Can't finish the operation, it is still in progress. (EWOULDBLOCK, EAGAIN) + * + * # Implementors note + * Unlike in POSIX, in WASI the bind operation is async. This enables + * interactive WASI hosts to inject permission prompts. Runtimes that + * don't want to make use of this ability can simply call the native + * `bind` as part of either `start-bind` or `finish-bind`. + * + * # References + * - + * - + * - + * - + */ + startBind(network: Network, localAddress: IpSocketAddress): void; + finishBind(): void; + /** + * Set up inbound & outbound communication channels, optionally to a specific peer. + * + * This function only changes the local socket configuration and does not generate any network traffic. + * On success, the `remote-address` of the socket is updated. The `local-address` may be updated as well, + * based on the best network path to `remote-address`. + * + * When a `remote-address` is provided, the returned streams are limited to communicating with that specific peer: + * - `send` can only be used to send to this destination. + * - `receive` will only return datagrams sent from the provided `remote-address`. + * + * This method may be called multiple times on the same socket to change its association, but + * only the most recently returned pair of streams will be operational. Implementations may trap if + * the streams returned by a previous invocation haven't been dropped yet before calling `stream` again. + * + * The POSIX equivalent in pseudo-code is: + * ```text + * if (was previously connected) { + * connect(s, AF_UNSPEC) + * } + * if (remote_address is Some) { + * connect(s, remote_address) + * } + * ``` + * + * Unlike in POSIX, the socket must already be explicitly bound. + * + * # Typical errors + * - `invalid-argument`: The `remote-address` has the wrong address family. (EAFNOSUPPORT) + * - `invalid-argument`: The IP address in `remote-address` is set to INADDR_ANY (`0.0.0.0` / `::`). (EDESTADDRREQ, EADDRNOTAVAIL) + * - `invalid-argument`: The port in `remote-address` is set to 0. (EDESTADDRREQ, EADDRNOTAVAIL) + * - `invalid-state`: The socket is not bound. + * - `address-in-use`: Tried to perform an implicit bind, but there were no ephemeral ports available. (EADDRINUSE, EADDRNOTAVAIL on Linux, EAGAIN on BSD) + * - `remote-unreachable`: The remote address is not reachable. (ECONNRESET, ENETRESET, EHOSTUNREACH, EHOSTDOWN, ENETUNREACH, ENETDOWN, ENONET) + * - `connection-refused`: The connection was refused. (ECONNREFUSED) + * + * # References + * - + * - + * - + * - + */ + stream(remoteAddress: IpSocketAddress | undefined): [IncomingDatagramStream, OutgoingDatagramStream]; + /** + * 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 + * - + * - + * - + * - + */ + localAddress(): IpSocketAddress; + /** + * Get the address the socket is currently streaming to. + * + * # Typical errors + * - `invalid-state`: The socket is not streaming to a specific remote address. (ENOTCONN) + * + * # References + * - + * - + * - + * - + */ + remoteAddress(): IpSocketAddress; + /** + * Whether this is a IPv4 or IPv6 socket. + * + * Equivalent to the SO_DOMAIN socket option. + */ + addressFamily(): IpAddressFamily; + /** + * 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. + */ + unicastHopLimit(): number; + setUnicastHopLimit(value: number): void; + /** + * 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. + */ + receiveBufferSize(): bigint; + setReceiveBufferSize(value: bigint): void; + sendBufferSize(): bigint; + setSendBufferSize(value: bigint): void; + /** + * Create a `pollable` which will resolve once the socket is ready for I/O. + * + * Note: this function is here for WASI 0.2 only. + * It's planned to be removed when `future` is natively supported in Preview3. + */ + subscribe(): Pollable; + } + \ No newline at end of file diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/io.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/io.d.ts new file mode 100644 index 0000000..7a9b351 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/io.d.ts @@ -0,0 +1,3 @@ +export type * as error from './interfaces/wasi-io-error.d.ts'; +export type * as poll from './interfaces/wasi-io-poll.d.ts'; +export type * as streams from './interfaces/wasi-io-streams.d.ts'; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/random.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/random.d.ts new file mode 100644 index 0000000..cffbc45 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/random.d.ts @@ -0,0 +1,3 @@ +export type * as insecureSeed from './interfaces/wasi-random-insecure-seed.d.ts'; +export type * as insecure from './interfaces/wasi-random-insecure.d.ts'; +export type * as random from './interfaces/wasi-random-random.d.ts'; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/sockets.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/sockets.d.ts new file mode 100644 index 0000000..d0a9807 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/sockets.d.ts @@ -0,0 +1,7 @@ +export type * as instanceNetwork from './interfaces/wasi-sockets-instance-network.d.ts'; +export type * as ipNameLookup from './interfaces/wasi-sockets-ip-name-lookup.d.ts'; +export type * as network from './interfaces/wasi-sockets-network.d.ts'; +export type * as tcpCreateSocket from './interfaces/wasi-sockets-tcp-create-socket.d.ts'; +export type * as tcp from './interfaces/wasi-sockets-tcp.d.ts'; +export type * as udpCreateSocket from './interfaces/wasi-sockets-udp-create-socket.d.ts'; +export type * as udp from './interfaces/wasi-sockets-udp.d.ts'; diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/wasi-cli-command.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/wasi-cli-command.d.ts new file mode 100644 index 0000000..1628ee2 --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/wasi-cli-command.d.ts @@ -0,0 +1,29 @@ +// world wasi:cli/command@0.2.3 +export type * as WasiCliEnvironment023 from './interfaces/wasi-cli-environment.js'; // import wasi:cli/environment@0.2.3 +export type * as WasiCliExit023 from './interfaces/wasi-cli-exit.js'; // import wasi:cli/exit@0.2.3 +export type * as WasiCliStderr023 from './interfaces/wasi-cli-stderr.js'; // import wasi:cli/stderr@0.2.3 +export type * as WasiCliStdin023 from './interfaces/wasi-cli-stdin.js'; // import wasi:cli/stdin@0.2.3 +export type * as WasiCliStdout023 from './interfaces/wasi-cli-stdout.js'; // import wasi:cli/stdout@0.2.3 +export type * as WasiCliTerminalInput023 from './interfaces/wasi-cli-terminal-input.js'; // import wasi:cli/terminal-input@0.2.3 +export type * as WasiCliTerminalOutput023 from './interfaces/wasi-cli-terminal-output.js'; // import wasi:cli/terminal-output@0.2.3 +export type * as WasiCliTerminalStderr023 from './interfaces/wasi-cli-terminal-stderr.js'; // import wasi:cli/terminal-stderr@0.2.3 +export type * as WasiCliTerminalStdin023 from './interfaces/wasi-cli-terminal-stdin.js'; // import wasi:cli/terminal-stdin@0.2.3 +export type * as WasiCliTerminalStdout023 from './interfaces/wasi-cli-terminal-stdout.js'; // import wasi:cli/terminal-stdout@0.2.3 +export type * as WasiClocksMonotonicClock023 from './interfaces/wasi-clocks-monotonic-clock.js'; // import wasi:clocks/monotonic-clock@0.2.3 +export type * as WasiClocksWallClock023 from './interfaces/wasi-clocks-wall-clock.js'; // import wasi:clocks/wall-clock@0.2.3 +export type * as WasiFilesystemPreopens023 from './interfaces/wasi-filesystem-preopens.js'; // import wasi:filesystem/preopens@0.2.3 +export type * as WasiFilesystemTypes023 from './interfaces/wasi-filesystem-types.js'; // import wasi:filesystem/types@0.2.3 +export type * as WasiIoError023 from './interfaces/wasi-io-error.js'; // import wasi:io/error@0.2.3 +export type * as WasiIoPoll023 from './interfaces/wasi-io-poll.js'; // import wasi:io/poll@0.2.3 +export type * as WasiIoStreams023 from './interfaces/wasi-io-streams.js'; // import wasi:io/streams@0.2.3 +export type * as WasiRandomInsecureSeed023 from './interfaces/wasi-random-insecure-seed.js'; // import wasi:random/insecure-seed@0.2.3 +export type * as WasiRandomInsecure023 from './interfaces/wasi-random-insecure.js'; // import wasi:random/insecure@0.2.3 +export type * as WasiRandomRandom023 from './interfaces/wasi-random-random.js'; // import wasi:random/random@0.2.3 +export type * as WasiSocketsInstanceNetwork023 from './interfaces/wasi-sockets-instance-network.js'; // import wasi:sockets/instance-network@0.2.3 +export type * as WasiSocketsIpNameLookup023 from './interfaces/wasi-sockets-ip-name-lookup.js'; // import wasi:sockets/ip-name-lookup@0.2.3 +export type * as WasiSocketsNetwork023 from './interfaces/wasi-sockets-network.js'; // import wasi:sockets/network@0.2.3 +export type * as WasiSocketsTcpCreateSocket023 from './interfaces/wasi-sockets-tcp-create-socket.js'; // import wasi:sockets/tcp-create-socket@0.2.3 +export type * as WasiSocketsTcp023 from './interfaces/wasi-sockets-tcp.js'; // import wasi:sockets/tcp@0.2.3 +export type * as WasiSocketsUdpCreateSocket023 from './interfaces/wasi-sockets-udp-create-socket.js'; // import wasi:sockets/udp-create-socket@0.2.3 +export type * as WasiSocketsUdp023 from './interfaces/wasi-sockets-udp.js'; // import wasi:sockets/udp@0.2.3 +export * as run from './interfaces/wasi-cli-run.js'; // export wasi:cli/run@0.2.3 diff --git a/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/wasi-http-proxy.d.ts b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/wasi-http-proxy.d.ts new file mode 100644 index 0000000..e9d49aa --- /dev/null +++ b/packages/web-host/overrides/@bytecodealliance/preview2-shim/types/wasi-http-proxy.d.ts @@ -0,0 +1,13 @@ +// world wasi:http/proxy@0.2.3 +export type * as WasiCliStderr023 from './interfaces/wasi-cli-stderr.js'; // import wasi:cli/stderr@0.2.3 +export type * as WasiCliStdin023 from './interfaces/wasi-cli-stdin.js'; // import wasi:cli/stdin@0.2.3 +export type * as WasiCliStdout023 from './interfaces/wasi-cli-stdout.js'; // import wasi:cli/stdout@0.2.3 +export type * as WasiClocksMonotonicClock023 from './interfaces/wasi-clocks-monotonic-clock.js'; // import wasi:clocks/monotonic-clock@0.2.3 +export type * as WasiClocksWallClock023 from './interfaces/wasi-clocks-wall-clock.js'; // import wasi:clocks/wall-clock@0.2.3 +export type * as WasiHttpOutgoingHandler023 from './interfaces/wasi-http-outgoing-handler.js'; // import wasi:http/outgoing-handler@0.2.3 +export type * as WasiHttpTypes023 from './interfaces/wasi-http-types.js'; // import wasi:http/types@0.2.3 +export type * as WasiIoError023 from './interfaces/wasi-io-error.js'; // import wasi:io/error@0.2.3 +export type * as WasiIoPoll023 from './interfaces/wasi-io-poll.js'; // import wasi:io/poll@0.2.3 +export type * as WasiIoStreams023 from './interfaces/wasi-io-streams.js'; // import wasi:io/streams@0.2.3 +export type * as WasiRandomRandom023 from './interfaces/wasi-random-random.js'; // import wasi:random/random@0.2.3 +export * as incomingHandler from './interfaces/wasi-http-incoming-handler.js'; // export wasi:http/incoming-handler@0.2.3 diff --git a/packages/web-host/src/components/HomePage.tsx b/packages/web-host/src/components/HomePage.tsx index 61218dd..c49596d 100644 --- a/packages/web-host/src/components/HomePage.tsx +++ b/packages/web-host/src/components/HomePage.tsx @@ -60,10 +60,10 @@ export const HomePage = ({ onStartRepl }: HomePageProps) => { runs in browsers and terminals with different host implementations.

- Plugins like cat or ls can interact with a - virtual filesystem using the primitives of the languages they are - written in (like std::fs::read_dir or{" "} - std::fs::read_to_string). + Plugins like cat, ls or tee can + interact with a virtual filesystem using the primitives of the + languages they are written in (like std::fs::read_dir,{" "} + std::fs::read_to_string or std::fs::write).

Key Features

@@ -75,9 +75,10 @@ export const HomePage = ({ onStartRepl }: HomePageProps) => { Language-agnostic plugins (any language that compiles to WASM)
  • - Virtual filesystem: the I/O operations are forwarded via the{" "} - @bytecodealliance/preview2-shim/filesystem shim, which - shims wasi:filesystem + Virtual filesystem: the I/O operations are forwarded via a local + fork of @bytecodealliance/preview2-shim/filesystem, + which shims wasi:filesystem (yes, you can do{" "} + WRITE operations)
  • Modern React + TypeScript web interface
  • diff --git a/packages/web-host/src/components/ReplHistory.tsx b/packages/web-host/src/components/ReplHistory.tsx index fd202a9..922cf7b 100644 --- a/packages/web-host/src/components/ReplHistory.tsx +++ b/packages/web-host/src/components/ReplHistory.tsx @@ -23,6 +23,7 @@ export function ReplHistory({
                   {entry.stdin}
                 
    @@ -31,6 +32,7 @@ export function ReplHistory({
    @@ -40,6 +42,7 @@ export function ReplHistory({
                   className="bg-green-100 whitespace-pre-wrap before:content-[attr(data-status)] relative before:absolute before:right-0 before:top-0 word-break-word"
                   data-status={entry.status === "success" ? "✅" : "❌"}
                   data-stdtype="stdout"
    +              data-std-index={index}
                 >
                   {entry.stdout}
                 
    @@ -49,6 +52,7 @@ export function ReplHistory({ className="bg-red-100 whitespace-pre-wrap before:content-[attr(data-status)] relative before:absolute before:right-0 before:top-0 word-break-word" data-status={entry.status === "success" ? "✅" : "❌"} data-stdtype="stderr" + data-std-index={index} > {entry.stderr} diff --git a/packages/web-host/src/hooks/exampleCommands.ts b/packages/web-host/src/hooks/exampleCommands.ts index e663820..062c392 100644 --- a/packages/web-host/src/hooks/exampleCommands.ts +++ b/packages/web-host/src/hooks/exampleCommands.ts @@ -41,6 +41,16 @@ const commands = [ () => ls("data/processed/2024"), () => ls("documents"), () => cat("documents/config.json"), + "echo We can also write to files! 🔥", + "tee output.txt", + "cat output.txt", + "echo We can write to files in append mode!", + "tee -a output.txt", + "echo Some more text", + "tee -a output.txt", + "echo Even more text", + "tee -a output.txt", + "cat output.txt", "weather Paris", "man weather", "help", diff --git a/packages/web-host/src/utils/github.ts b/packages/web-host/src/utils/github.ts index a0b3038..cec7916 100644 --- a/packages/web-host/src/utils/github.ts +++ b/packages/web-host/src/utils/github.ts @@ -8,6 +8,7 @@ const pluginSourceUrlMapping = { cat: "https://github.com/topheman/webassembly-component-model-experiments/tree/master/crates/plugin-cat", echoc: "https://github.com/topheman/webassembly-component-model-experiments/blob/master/c_modules/plugin-echo/component.c", + tee: "https://github.com/topheman/webassembly-component-model-experiments/tree/master/crates/plugin-tee", } as const; export function getPluginSourceUrl( diff --git a/packages/web-host/src/wasm/engine.ts b/packages/web-host/src/wasm/engine.ts index 0768171..f9a7fee 100644 --- a/packages/web-host/src/wasm/engine.ts +++ b/packages/web-host/src/wasm/engine.ts @@ -41,6 +41,7 @@ async function loadPlugins({ import("./generated/plugin_greet/transpiled/plugin_greet.js"), import("./generated/plugin_ls/transpiled/plugin_ls.js"), import("./generated/plugin_cat/transpiled/plugin_cat.js"), + import("./generated/plugin_tee/transpiled/plugin_tee.js"), import("./generated/plugin-echo-c/transpiled/plugin-echo-c.js"), ]).then((plugins) => plugins.map((plugin) => { diff --git a/packages/web-host/tests/repl-loading.spec.ts b/packages/web-host/tests/repl-loading.spec.ts index aff40bf..46f0dc1 100644 --- a/packages/web-host/tests/repl-loading.spec.ts +++ b/packages/web-host/tests/repl-loading.spec.ts @@ -11,7 +11,7 @@ test("repl logic should have loaded", async ({ page }) => { }); test("plugins should have loaded under their names", async ({ page }) => { - const pluginNames = ["echo", "weather", "greet", "ls", "cat", "echoc"]; + const pluginNames = ["echo", "weather", "greet", "ls", "cat", "echoc", "tee"]; await page.goto("/#repl"); for (const pluginName of pluginNames) { await expect( diff --git a/packages/web-host/tests/repl-logic.spec.ts b/packages/web-host/tests/repl-logic.spec.ts index 8b72350..2e43f45 100644 --- a/packages/web-host/tests/repl-logic.spec.ts +++ b/packages/web-host/tests/repl-logic.spec.ts @@ -80,6 +80,7 @@ help reserved list-commands reserved ls plugin man reserved +tee plugin weather plugin`, ); }); diff --git a/packages/web-host/tests/repl-plugins.spec.ts b/packages/web-host/tests/repl-plugins.spec.ts index 084bc75..0e7355c 100644 --- a/packages/web-host/tests/repl-plugins.spec.ts +++ b/packages/web-host/tests/repl-plugins.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { fillAndSubmitCommand, getLastStd } from "./utils"; +import { fillAndSubmitCommand, getLastStd, sleep } from "./utils"; test("echo foo", async ({ page }) => { await page.goto("/#repl"); @@ -89,3 +89,53 @@ test("cat README.md", async ({ page }) => { You are interacting with a virtual filesystem, in your browser!`); }); + +test("tee new-file.txt", async ({ page }) => { + await page.goto("/#repl"); + // check the file don't exist + await fillAndSubmitCommand(page, "ls", { + expectStdout: `D data +D documents +D logs +F .config +F .hidden_file +F README.md`, + }); + await fillAndSubmitCommand(page, "echo Some Content"); + await sleep(); + await fillAndSubmitCommand(page, "tee new-file.txt", { + expectStdout: "Some Content", + }); + await fillAndSubmitCommand(page, "cat new-file.txt", { + expectStdout: "Some Content", + }); +}); + +test("tee README.md", async ({ page }) => { + await page.goto("/#repl"); + await fillAndSubmitCommand(page, "echo Some Content"); + await sleep(); + await fillAndSubmitCommand(page, "tee README.md", { + expectStdout: "Some Content", + }); + await fillAndSubmitCommand(page, "cat README.md", { + expectStdout: "Some Content", + }); +}); + +test("tee -a output.txt", async ({ page }) => { + await page.goto("/#repl"); + await fillAndSubmitCommand(page, "echo Some Initial Content"); + await sleep(); + await fillAndSubmitCommand(page, "tee output.txt", { + expectStdout: "Some Initial Content", + }); + await fillAndSubmitCommand(page, "echo Some More Content"); + await sleep(); + await fillAndSubmitCommand(page, "tee -a output.txt", { + expectStdout: "Some More Content", + }); + await fillAndSubmitCommand(page, "cat output.txt", { + expectStdout: "Some Initial Content\nSome More Content", + }); +}); diff --git a/packages/web-host/tests/utils.ts b/packages/web-host/tests/utils.ts index 78409b6..551bfce 100644 --- a/packages/web-host/tests/utils.ts +++ b/packages/web-host/tests/utils.ts @@ -1,13 +1,50 @@ -import { expect, type Page } from "@playwright/test"; +import { expect, type Locator, type Page } from "@playwright/test"; /** * Get the last std output of the given type + * If expectContent is provided, it will retry to get the last std output until it matches the expected content */ export async function getLastStd( page: Page, type: "stdin" | "stdout" | "stderr", + { + expectContent, + }: { + expectContent?: string; + } = {}, ) { - return await page.locator(`[data-stdtype='${type}']`).last(); + const locator = await page.locator(`[data-stdtype='${type}']`).last(); + if (expectContent) { + const text = await locator.textContent(); + if (text?.includes(expectContent)) { + return locator; + } + // if no match, do a hard expect that will fail the test with a clear error message + // Sorry you landed here, you will most likely have to add some `sleep()` in your code 🥲 + await expect(locator).toHaveText(expectContent); + } + return locator; +} + +/** + * Get the last std output of the given type after the given locator + * This is useful to get the last std output after a command has been submitted + * This ensures you don't have false positives when checking the last std output + */ +export async function getLastStdAfter( + page: Page, + type: "stdin" | "stdout" | "stderr", + stdLocator: Locator, +) { + const stdinIndex = await stdLocator.getAttribute("data-std-index"); + return await page + .locator(`[data-std-index='${stdinIndex}'] ~ [data-stdtype='${type}']`) + .last(); +} + +export async function sleep(ms?: number): Promise { + const DEFAULT_DELAY = 200; // taking into account the default delay necessary in the CI + return new Promise((resolve) => setTimeout(resolve, ms ?? DEFAULT_DELAY)); } /** @@ -18,26 +55,33 @@ export async function fillAndSubmitCommand( page: Page, command: string, { - expectStdin = command, + expectStdin, expectStdout, expectStderr, + afterSubmit, }: { expectStdin?: string; expectStdout?: string; expectStderr?: string; + afterSubmit?: () => Promise; } = {}, ) { + const expectedStdin = expectStdin ?? command; const input = await page.getByPlaceholder("Type a command..."); await input.fill(command); await input.press("Enter"); - const stdin = await getLastStd(page, "stdin"); - await expect(stdin).toHaveText(expectStdin); + if (afterSubmit) { + await afterSubmit(); + } + const stdin = await getLastStd(page, "stdin", { + expectContent: expectedStdin, + }); if (expectStdout) { - const stdout = await getLastStd(page, "stdout"); + const stdout = await getLastStdAfter(page, "stdout", stdin); await expect(stdout).toHaveText(expectStdout); } if (expectStderr) { - const stderr = await getLastStd(page, "stderr"); + const stderr = await getLastStdAfter(page, "stderr", stdin); await expect(stderr).toHaveText(expectStderr); } } @@ -50,7 +94,7 @@ export async function clickWandButton( page: Page, command: string, { - expectStdin = command, + expectStdin, expectStdout, expectStderr, }: { @@ -59,13 +103,15 @@ export async function clickWandButton( expectStderr?: string; } = {}, ) { + const expectedStdin = expectStdin ?? command; await page.getByTitle("Run example command").click({ force: true }); const input = await page.getByPlaceholder("Type a command..."); - await expect(input).toHaveValue(expectStdin); - const stdin = await getLastStd(page, "stdin"); - await expect(stdin).toHaveText(expectStdin); + await expect(input).toHaveValue(expectedStdin); + const stdin = await getLastStd(page, "stdin", { + expectContent: expectedStdin, + }); if (expectStdout) { - const stdout = await getLastStd(page, "stdout"); + const stdout = await getLastStdAfter(page, "stdout", stdin); await expect(stdout).toHaveText(expectStdout); } if (expectStderr) { diff --git a/packages/web-host/tsconfig.app.json b/packages/web-host/tsconfig.app.json index 3a2dc0f..431fad9 100644 --- a/packages/web-host/tsconfig.app.json +++ b/packages/web-host/tsconfig.app.json @@ -25,8 +25,11 @@ "paths": { "repl:api/host-state": ["./src/wasm/host/host-state.ts"], "repl:api/http-client": ["./src/wasm/host/http-client.ts"], - "repl:api/host-state-plugin": ["./src/wasm/host/host-state-plugin.ts"] + "repl:api/host-state-plugin": ["./src/wasm/host/host-state-plugin.ts"], + "@bytecodealliance/preview2-shim": [ + "./overrides/@bytecodealliance/preview2-shim" + ] } }, - "include": ["src", "clis"] + "include": ["src", "clis", "overrides"] } diff --git a/packages/web-host/vite.config.ts b/packages/web-host/vite.config.ts index c0730a9..b96618b 100644 --- a/packages/web-host/vite.config.ts +++ b/packages/web-host/vite.config.ts @@ -20,6 +20,10 @@ export default defineConfig({ __dirname, "./src/wasm/host/host-state-plugin.ts", ), + "@bytecodealliance/preview2-shim": path.resolve( + __dirname, + "./overrides/@bytecodealliance/preview2-shim/lib/browser", + ), }, }, base: "/webassembly-component-model-experiments/",