wRPC is a transport-agnostic protocol designed for asynchronous transmit of WIT function call invocations over network and other means of communication.
wRPC follows client-server model, where peers (servers) may serve function and method calls invoked by the other peers (clients).
wRPC relies on component model value definition encoding for data encoding on the wire.
wRPC makes use of transports, which are responsible for establishing a connection between two parties and transferring the wRPC wire protocol data between them.
A transport MAY support a native half-close, which lets a peer communicate the end of transmit while it keeps receiving the data sent by the other peer. A peer using a transport, which can only close both directions at once, MUST keep the byte stream open until it is done sending and receiving.
Examples of supported wRPC transports are: TCP, Unix Domain Sockets, QUIC, WebTransport and HTTP (see Transport specifications).
As WIT interfaces and associated values are asynchronous in nature, callers and callees require ability to asynchronously, bidirectionally transfer (portions of) function parameter and result value data.
For example, caller of wasi:http/outgoing-handler.handle function MUST be able to simultaneously send data over the passed output-stream as well as receive data from the returned input-stream. For this purpose wRPC multiplexes these data streams over a single bidirectional byte stream provided by the transport, allowing for bidirectional concurrent transfer of multiple data streams (see Framing).
wRPC uses a concept of "indexing" for differentiating and identifying the data streams used as part of processing of a single WIT function invocation.
An "index" is a sequence of unsigned 32-bit integers and represents a reflective structural path to the value, e.g. a record field or a list element. The "root" (synchronous) data channel is identified by the empty index [].
Consider the following WIT:
package wrpc-example:doc@0.1.0;
interface example {
record rec {
a: stream<u8>,
b: u32,
}
foo: func(v: rec) -> stream<u8>;
}A path to field a in foo parameter v is defined as a sequence: [0, 0].
The invoker of foo MAY choose to send the whole contents of parameter v, i.e. rec encoded using component model value definition encoding on the "root" (synchronous) parameter data channel, in which case stream<u8> is sent as ready list<u8> (see Streams), otherwise, the invoker MAY mark rec.a as pending in the encoding of v and instead, send it asynchronously over the data channel identified by [0, 0] (first field in the first parameter).
Similarly, the handler of foo MAY either send the complete resulting stream<u8> contents as encoded ready list<u8> over the "root" (synchronous) result data channel or asynchronously over the data channel identified by [0] (first return value).
The indexing rules are as follows:
- Record fields are indexed in order of their WIT declaration
- Tuple members are indexed in order of their WIT declaration
- Variant, option, result and future payloads are not indexed
- List elements are indexed in the order they appear in the list
- Stream elements are indexed in the order they appear in the stream
Transports provide a single bidirectional byte stream per wRPC invocation. wRPC defines a mandatory framing format, described below, that multiplexes the asynchronous data streams of an invocation (each identified by its index) over that single byte stream.
All wRPC transports use this framing. A transport implementation only needs to provide a single bidirectional byte stream; the framing layer supplies the multiplexing, so transports do not implement their own indexing scheme.
The framed stream sent by the client begins with a version byte 0x01 and is followed by a header encoded using component model value definition encoding:
record header {
instance: string,
name: string,
}The header MAY be followed by one or more frames encoded using component model value definition encoding. The framed stream sent by the server consists of frames only:
record frame {
path: list<u32>,
data: list<u8>,
}An empty frame, i.e. a frame with empty data, closes its path: the sender MUST NOT send any more frames on that path. If a frame is received on a closed path, the receiver MUST treat it as an error. Closing a path does not close the paths nested in it. A sender MAY leave a path open, unless stated otherwise (see Futures).
Likewise, if a frame is received on a path that carries no pending value the receiver has received or may still receive, the receiver MUST treat it as an error.
A sender MUST send the data of a pending value only after the data carrying it, for example, the data of a pending value in a stream element after the stream chunk containing that element, since a receiver MAY bound the data it buffers for a path, which no value it decoded carries yet.
Frames do not communicate the end of transmit, i.e. that no more frames follow. A transport communicates it using its native half-close, if it has one, for example, TCP shutdown or finishing a QUIC stream, and otherwise by closing the byte stream (see Transport). The end of transmit closes every path.
A receiver, which treats received data as an error, MUST fail the invocation. A peer, which fails an invocation, MUST close the invocation's byte stream in both directions using the transport's mechanism (see Transport specifications), and MUST NOT close a connection or session carrying other invocations.
TCP relies on Framed stream specification to map a single TCP stream to a single wRPC invocation.
The server MUST listen on a TCP socket and client MUST establish a new connection to that socket per each invocation.
The write side of the stream MUST be shutdown as soon as data transfer is done, for example, once the client is done sending encoded parameter buffer and all asynchronous parameters, it MUST shutdown the write side of the stream to signal EOF to the server.
To close the stream in both directions (see Framed stream specification), a peer MUST reset the connection (RST).
This mapping is deliberate: a TCP connection provides exactly one byte stream and no stream-level multiplexing, so the TCP transport does not multiplex invocations over a connection itself. Since the client shuts down its write side first, the client performs the active close and holds a TIME_WAIT socket per invocation, which bounds the sustained invocation rate from a single client to a single server address by the size of the client's ephemeral port range.
Peers that need many concurrent invocations over a single long-lived connection, or per-invocation metadata, SHOULD use a transport that natively multiplexes streams, such as QUIC, WebTransport or HTTP over HTTP/2.
Unix domain sockets follow the TCP transport specification, using a stream-oriented (SOCK_STREAM) socket in place of a TCP socket and closing the socket in place of resetting the connection.
QUIC relies on Framed stream specification to map a single bidirectional QUIC stream to a single wRPC invocation.
The client MUST open a new client-initiated bidirectional stream per each invocation. A single QUIC connection MAY carry multiple concurrent invocations.
A peer MUST communicate the end of transmit by finishing the send side of the stream.
Once a peer is done receiving data, it SHOULD stop the receive side of the stream with application error code 0. A peer MUST treat its send side being stopped with application error code 0 after it finished the stream as successful completion.
To close the stream in both directions (see Framed stream specification), a peer MUST stop the receive side and reset the send side of the stream, both with application error code 1, which indicates a failed invocation.
TLS configuration, including the ALPN protocol, is application-defined.
WebTransport follows the QUIC transport specification, using bidirectional streams opened by the client within a WebTransport session in place of QUIC streams and WebTransport application error codes in place of QUIC application error codes.
A single WebTransport session MAY carry multiple concurrent invocations.
HTTP relies on Framed stream specification to map a single HTTP request-response exchange to a single wRPC invocation.
The client MUST send a POST request per each invocation, with the request body carrying the data sent by the client and the response body carrying the data sent by the server. The end of the request or response body communicates the end of transmit.
To close the exchange in both directions (see Framed stream specification), a peer MUST reset its stream with an error code other than NO_ERROR, for example, RST_STREAM with INTERNAL_ERROR in HTTP/2. Where the exchange has no stream of its own, as in HTTP/1.1, a peer MUST close the connection instead.
The request target and header fields are application-defined and not interpreted by wRPC.
A server accepting the invocation MUST respond with a 2xx status code without waiting for the request body to complete, and the client MUST NOT wait for the response before sending the request body. A client MUST treat a response with a status code other than 2xx as a failed invocation.
Since data is transferred concurrently in both directions, the HTTP version used SHOULD support full-duplex exchange, for example, HTTP/2.
future<T> values are encoded as variant future<T> { pending, ready(T) }.
In case a future is pending, its value is transmitted using the future's index.
For example:
foo: func(v: future<bool>);If v is pending, encoded bool value is sent on index [0] (corresponding to first parameter)
A future without a payload type has an empty value encoding, so once such a pending future resolves, the sender MUST close its index, either with an empty frame or by ending transmit (see Framed stream specification), and the receiver MUST consider it resolved once the index is closed.
stream<T> values are encoded as variant stream<T> { pending, ready(list<T>) }.
A ready stream contains all of the stream's elements, i.e. the stream is finished. An empty ready stream is a finished stream containing no elements.
In case a stream is pending, it is transmitted as a sequence of list<T> chunks using the parent's index.
Each pending stream MUST finish with an empty list<T>. Because an empty chunk marks the end of the stream, a sender MUST NOT send an empty chunk before the stream is finished.
Resources are encoded as opaque byte blobs, list<u8> and their meaning is entirely application specific.