@portable/rpc
Functions
Rpc.connect
Connects to an API served on the other side of transport and returns a proxy for it. Calling
a method sends a JSON-RPC request and returns a promise for the result. Pass methods to answer
calls from the other side over the same transport.
Where the runtime has Symbol.asyncDispose, the proxy's [Symbol.asyncDispose]() closes the
connection as aborting signal does, with an AbortError DOMException, so a remote declared
with await using is closed when the scope ends.
function connect<api>(
transport: Transport,
options: connect.Options = {},
): Remote<api> & AsyncDisposeMethod;| Option | Type | Default | Description |
|---|---|---|---|
codec | Codec<any> | Rpc.json | Encodes messages for the transport. |
methods | object | Methods the other side can call over the same transport. | |
signal | AbortSignal | Closes the connection: waiting calls reject with signal.reason, and the transport is cancelled. |
import * as Rpc from '@portable/rpc';
import type { Api } from './worker.ts';
const worker = Rpc.connect<Api>(
Rpc.port(new Worker('./worker.ts', { type: 'module' })),
);
await worker.add(1, 2);
for await (const event of worker.watch('x')) {
console.log(event);
}Rpc.provide
Answers calls to methods over transport: every method, including methods of nested objects as
'name.method', can be called with JSON-RPC. A method that returns an async iterable or a
ReadableStream streams its values back with backpressure.
function provide(
transport: Transport,
methods: object,
options: provide.Options = {},
): void;| Option | Type | Default | Description |
|---|---|---|---|
codec | Codec<any> | Rpc.json | Encodes messages for the transport. |
signal | AbortSignal | Closes the connection: waiting calls reject with signal.reason, and the transport is cancelled. |
import * as Rpc from '@portable/rpc';
const api = {
add: (a: number, b: number) => a + b,
async *watch(id: string) {
for (let i = 0; ; i++) {
yield { id, i };
}
},
};
export type Api = typeof api;
Rpc.provide(Rpc.port(self), api);Rpc.handler
Answers calls to methods over HTTP. The returned function answers a POST of a JSON-RPC request or batch. The
response is JSON, or, when a method returns a stream, newline-delimited messages that carry the
stream's values as they are produced. Methods wrapped with withContext get the Request, and
their signal aborts when the client disconnects.
function handler(
methods: object,
): (request: Request) => Promise<Response>;import * as Rpc from '@portable/rpc';
const rpc = Rpc.handler(api);
export default { fetch: (request: Request) => rpc(request) };Rpc.http
Connects to an API served by handler, or any JSON-RPC server that accepts POST requests. Calls
made in the same tick are sent as one batch request. Where the runtime has
Symbol.asyncDispose, the remote's [Symbol.asyncDispose]() aborts every request and rejects
waiting calls with an AbortError DOMException, as aborting signal does.
function http<api>(
url: string | URL,
options: http.Options = {},
): Remote<api> & AsyncDisposeMethod;| Option | Type | Default | Description |
|---|---|---|---|
fetch | ((request: Request) => Promise<Response>) | globalThis.fetch | The fetch to send requests with, such as one that adds an authorization header. |
signal | AbortSignal | Aborts every request and rejects waiting calls with signal.reason. |
import * as Rpc from '@portable/rpc';
import type { Api } from './server.ts';
const api = Rpc.http<Api>('https://example.com/rpc');
await api.add(1, 2);Rpc.port
A transport over postMessage: a MessagePort, a Worker, a BroadcastChannel, or self
inside a worker. Messages are structured-cloned, so the identity codec
{ encode: (m) => m, decode: (m) => m } also works and keeps Date, Map and Uint8Array
values intact.
function port(
target: port.Target,
options: port.Options = {},
): Transport;| Option | Type | Default | Description |
|---|---|---|---|
signal | AbortSignal | Stops listening and errors the readable with signal.reason. |
import * as Rpc from '@portable/rpc';
const { port1, port2 } = new MessageChannel();
Rpc.provide(Rpc.port(port1), { add: (a: number, b: number) => a + b });
const remote = Rpc.connect<{ add(a: number, b: number): number }>(
Rpc.port(port2),
);
await remote.add(1, 2);Rpc.call
Calls a remote method with explicit parameters and options. Use it to pass a signal, or to send
parameters by name (an object), as MCP and LSP servers expect.
function call<fn extends (...args: any[]) => unknown>(
method: fn,
params: CallParams<fn>,
options: call.Options = {},
): ReturnType<fn>;| Option | Type | Default | Description |
|---|---|---|---|
signal | AbortSignal | Cancels the call: the promise rejects with signal.reason, and the client sends $/cancel to the remote. |
import * as Rpc from '@portable/rpc';
const sum = await Rpc.call(remote.add, [1, 2], {
signal: AbortSignal.timeout(5000),
});
const mcp = Rpc.connect<any>(transport);
const { tools } = await Rpc.call(mcp['tools/list'], {});Rpc.notify
Sends a JSON-RPC notification: a call without an id, which the remote doesn't answer. The promise resolves once the message is written.
function notify<fn extends (...args: any[]) => unknown>(
method: fn,
params: CallParams<fn>,
options: notify.Options = {},
): Promise<void>;| Option | Type | Default | Description |
|---|---|---|---|
signal | AbortSignal | Rejects with signal.reason instead of sending when already aborted. |
import * as Rpc from '@portable/rpc';
await Rpc.notify(remote.log, ['page loaded']);
await Rpc.notify(mcp['notifications/initialized'], {});Rpc.withContext
Wraps a method so that it receives a Context as its first argument when it is called
remotely. The context's signal aborts when the caller cancels the call or the connection
closes. Callers see the method without the context argument.
function withContext<args extends unknown[], result>(
fn: (context: Context, ...args: args) => result,
): (...args: args) => result;import * as Rpc from '@portable/rpc';
const api = {
search: Rpc.withContext(async ({ signal }, query: string) => {
const response = await fetch(
`https://example.com/search?q=${query}`,
{ signal },
);
return response.json();
}),
};Rpc.window
A transport to another window, such as an iframe or a popup. Both sides send a hello every 100 ms
until the other answers, so either may start first, and writes wait until the handshake is done.
The handshake has no timeout: give each call a signal to stop waiting for a window that never
answers.
function window(
target: window.Target,
options: window.Options,
): Transport;| Option | Type | Default | Description |
|---|---|---|---|
origin | string | The other window's origin, such as 'https://app.example.com'. Messages from any other origin are ignored, and messages are posted only to this origin. '*' accepts and sends to any origin. | |
channel | string | '' | Separates several connections to the same window. Both sides must use the same channel. |
signal | AbortSignal | Stops the handshake and listening, and errors the readable with signal.reason. |
import * as Rpc from '@portable/rpc';
// In the parent page
const iframe = document.querySelector('iframe')!;
const child = Rpc.connect<ChildApi>(
Rpc.window(iframe.contentWindow!, {
origin: 'https://child.example.com',
}),
);
// In the iframe
Rpc.provide(
Rpc.window(window.parent, { origin: 'https://parent.example.com' }),
api,
);Constants
Rpc.json
The default codec: JSON text. It decodes text or UTF-8 bytes, so it reads binary WebSocket frames too.
const json: Codec<string | Uint8Array>;import * as Rpc from '@portable/rpc';
import * as cbor from 'cborg';
Rpc.connect(transport, { codec: Rpc.json });
Rpc.connect(transport, {
codec: { encode: cbor.encode, decode: cbor.decode },
});Errors
Rpc.RemoteError
A JSON-RPC error. A client rejects with it when the remote answers with an error, and a method
throws it to send a chosen code and data. Any other error a method throws is sent with code
-32603 and its message.
class RemoteError extends Errors.BaseError {
constructor(options: RemoteError.Options);
readonly kind: string;
readonly code: number;
readonly data: unknown;
}| Option | Type | Default | Description |
|---|---|---|---|
code | number | ||
message | string | ||
data | unknown | ||
cause | unknown |
import * as Rpc from '@portable/rpc';
const api = {
user(id: string) {
throw new Rpc.RemoteError({
code: 404,
message: 'No such user',
data: { id },
});
},
};
try {
await remote.user('x');
} catch (error) {
if (error instanceof Rpc.RemoteError && error.code === 404) {
return null;
}
throw error;
}Rpc.TransportError
The error that waiting calls reject with when the transport closes or fails, or when an HTTP
request fails. cause holds the underlying error, which is an Errors.ResponseError for a failed HTTP response.
class TransportError extends Errors.BaseError {
readonly kind: string;
}import * as Rpc from '@portable/rpc';
try {
await remote.save(doc);
} catch (error) {
if (error instanceof Rpc.TransportError) {
return retryLater(doc);
}
throw error;
}Types
Rpc.Codec
Turns messages into what a transport carries, and back. decode may return anything:
malformed messages are answered with JSON-RPC errors.
type Codec<encoded = unknown> = {
encode(message: Message): encoded;
decode(data: encoded): unknown;
};Rpc.Context
What a method wrapped with withContext receives as its first argument.
type Context = {
readonly signal: AbortSignal;
readonly request?: Request | undefined;
};Rpc.Message
A JSON-RPC 2.0 message object, or a batch of them.
type Message =
| RequestMessage
| NotificationMessage
| ResponseMessage
| readonly (RequestMessage | NotificationMessage)[]
| readonly ResponseMessage[];Rpc.Remote
The type of a remote API. Every method returns a promise, and a method that returns an async
iterable or a ReadableStream returns a promise for a ReadableStream that can also be
iterated with for await directly.
type Remote<api> = extends & api ? any : {
readonly [key in keyof api]: api[key] extends (...args: infer args) => infer result ? (...args: args) => Result<Awaited<result>> : api[key] extends object ? Remote<api[key]> : never;
};Rpc.Result
What a remote method returns for a result of type value.
type Result<value> = [value] extends [
AsyncIterable<infer item> | ReadableStream<infer item>,
]
? Promise<ReadableStream<item>> & AsyncIterable<item>
: Promise<value>;Rpc.Transport
A pair of streams that carry encoded messages, such as a WebSocket wrapped in streams.
type Transport = {
readonly readable: ReadableStream<unknown>;
readonly writable: WritableStream<unknown>;
};