RPC Anywhere ships with a few built-in ways to create transports for common use cases.
For example, a transport for browser extensions (content script ↔ service worker) can be created with createTransportFromBrowserRuntimePort(port)
. The transport can then be passed to createRPC
or lazily set on an existing RPC instance with setTransport(transport)
.
import { createTransportFromBrowserRuntimePort } from "rpc-anywhere";
const port = browser.runtime.connect({ name: "my-rpc-port" });
const rpc = createRPC<ScriptSchema, WorkerSchema>({
transport: createTransportFromBrowserRuntimePort(port),
// ...
});
// or
const rpc = createRPC<ScriptSchema, WorkerSchema>({
// ...
});
rpc.setTransport(createTransportFromBrowserRuntimePort(port));
A full list of built-in transports can be found below.
- Iframes
- Browser extensions
- Workers
- Broadcast channels
- Message ports: windows, workers, broadcast channels
export async function createIframeTransport(
iframe: HTMLIFrameElement,
options?: {
transportId?: string | number;
filter?: (event: MessageEvent) => boolean;
targetOrigin?: string; // default: "*"
},
): Promise<RPCTransport>;
export async function createIframeParentTransport(options?: {
transportId?: string | number;
filter?: (event: MessageEvent) => boolean;
}): Promise<RPCTransport>;
Create transports that enable communication between an iframe and its parent window. The connection itself is fully created and managed by using MessageChannel
under the hood, you only need to provide the target iframe element in the parent window.
createIframeTransport
is used from the parent window, and it creates a transport that exchanges messages with the child iframe.createIframeParentTransport
is used from the child iframe, and it creates a transport that exchanges messages with the parent window.
These functions are asynchronous because the following steps need to be followed to establish the connection:
- The parent window waits for the iframe element and content to load, and then sends an "initialization" message to the iframe along with a message port.
- The child iframe waits for the "initialization" message, stores the port for future use by the transport, and sends a "ready" message back to the parent window.
- The parent window waits for the "ready" message.
This process ensures that the connection is established and ready to use before creating the transports at both ends. Once completed, the transport can be immediately used.
Using the transportId
option is recommended to avoid potential conflicts with other messages. It must be unique and match on both ends. If security is a concern, the targetOrigin
option should be set to the expected origin of the iframe.
In the parent window:
import { createIframeTransport } from "rpc-anywhere";
const iframeElement = document.getElementById("my-iframe") as HTMLIFrameElement;
createIframeTransport(iframeElement, { transportId: "my-transport" }).then(
(transport) => {
const rpc = createRPC<Schema>({
transport,
// ...
});
// ...
},
);
In the child iframe:
import { createIframeParentTransport } from "rpc-anywhere";
createIframeParentTransport({ transportId: "my-transport" }).then(
(transport) => {
const rpc = createRPC<Schema>({
transport,
// ...
});
// ...
},
);
function createTransportFromBrowserRuntimePort(
port: Browser.Runtime.Port | Chrome.runtime.Port,
options?: {
transportId?: string | number;
filter?: (message: any, port: Browser.Runtime.Port) => boolean;
},
): RPCTransport;
Create transports between different contexts in a web extension using browser runtime ports. A common example is between a content script and a service worker. Learn more on MDN.
Note that you'll need to understand and manage the creation and lifecycle of the ports yourself on both ends of the connection.
It is recommended to use a port that has a unique name and is used exclusively for the RPC connection. If the port is also used for other purposes, the RPC instance might receive messages that are not intended for it.
If you need to share a port, you can use the transportId
option to ensure that only messages that match that specific ID are handled. For advanced use cases, you can use the filter
option to filter messages dynamically. The filter function will be called with the (low-level) message object and the port, and should return true
if the message should be handled, and false
otherwise.
This example involves a connection that is established from a content script to a service worker.
Other sorts of connections are possible like in the opposite direction (from a service worker to a content script), to a different extension, or to a native application. The MDN page linked above provides more information on the different types of connection APIs.
In a content script:
import { createTransportFromBrowserRuntimePort } from "rpc-anywhere";
const port = browser.runtime.connect({ name: "my-rpc-port" });
const rpc = createRPC<ScriptSchema, WorkerSchema>({
transport: createTransportFromBrowserRuntimePort(port),
// ...
});
// ...
In a service worker:
import { createTransportFromBrowserRuntimePort } from "rpc-anywhere";
browser.runtime.onConnect.addListener((port) => {
if (port.name === "my-rpc-port") {
const rpc = createRPC<WorkerSchema, ScriptSchema>({
transport: createTransportFromBrowserRuntimePort(port),
// ...
});
// ...
}
});
export function createWorkerTransport(
worker: Worker,
options?: {
transportId?: string | number;
filter?: (event: MessageEvent) => boolean;
},
): RPCTransport;
export function createWorkerParentTransport(
worker: Worker,
options?: {
transportId?: string | number;
filter?: (event: MessageEvent) => boolean;
},
): RPCTransport;
Create transports between a worker and its parent context.
createWorkerTransport
is used from the parent context, and it creates a transport that exchanges messages with the worker.createWorkerParentTransport
is used from the worker, and it creates a transport that exchanges messages with the parent context.
The transportId
option can be used to avoid potential conflicts with other messages and transports. It must be unique and match on both ends.
In the parent context:
import { createWorkerTransport } from "rpc-anywhere";
const worker = new Worker("worker.js");
const rpc = createRPC<Schema>({
transport: createWorkerTransport(worker),
// ...
});
// ...
In the worker:
import { createWorkerParentTransport } from "rpc-anywhere";
const rpc = createRPC<Schema>({
transport: createWorkerParentTransport(),
// ...
});
// ...
export function createTransportFromBroadcastChannel(
channel: BroadcastChannel,
options?: {
transportId?: string | number;
filter?: (event: MessageEvent) => boolean;
},
): RPCTransport;
Create transports from broadcast channels.
A BroadcastChannel
can be used to communicate between different windows, tabs, iframes, workers... It can be used to send and receive messages to/from all other BroadcastChannel
objects with the same name.
Broadcast channels can be tricky because there can be more than two instances of the same channel, and messages are received by all of them. While RPC Anywhere is not necessarily limited to a connection between only two endpoints, this is an advanced pattern that requires careful consideration.
To avoid issues, it is recommended to avoid using requests since they are designed for one-to-one communication (unless you know what you're doing). Sending messages is perfectly fine, and will be received by all other instances of the channel.
import { createTransportFromBroadcastChannel } from "rpc-anywhere";
const channel = new BroadcastChannel("my-channel");
const rpc = createRPC<Schema>({
transport: createTransportFromBroadcastChannel(channel),
// ...
});
// ...
Warning: this API is low-level and requires a good understanding of the target environment and its APIs. It is recommended to use the higher-level APIs whenever possible:
- For iframes, you can use
createIframeTransport
andcreateIframeParentTransport
.- For workers, you can use
createWorkerTransport
andcreateWorkerParentTransport
.- For broadcast channels, you can use
createTransportFromBroadcastChannel
.
export function createTransportFromMessagePort(
port:
| MessagePort
| Window
| Worker
| ServiceWorkerContainer
| BroadcastChannel,
options?: {
transportId?: string | number;
filter?: (event: MessageEvent) => boolean;
remotePort?:
| MessagePort
| Window
| Worker
| ServiceWorker
| Client
| BroadcastChannel;
},
): RPCTransport;
Create transports from message ports.
Works with MessagePort
instances and any objects that implement a similar interface: addEventListener("message", listener)
and postMessage(message)
. This is the case for window objects (including iframes), different types of workers, and broadcast channels. Here's a quick breakdown:
- Window: the global
window
or thecontentWindow
of an iframe. - Worker: a web worker. Other kinds of workers (like service workers and worklets) are also supported through their respective interfaces.
- BroadcastChannel: a special kind of message port that can send messages to all other
BroadcastChannel
objects with the same name. It can be used to communicate between different windows, tabs, iframes, workers...
In most cases, all inbound and outbound messages are handled by the same port. However, in some cases, inbound messages are handled by one port, and outbound messages are sent to another. For example, this is the case for parent and iframe windows, where messages are received by the parent's window (window.addEventListener("message", listener)
) but sent through the iframe's window (iframe.contentWindow.postMessage(message)
).
In those cases, you can use the remotePort
option to specify the port that outgoing messages will be sent to.
When creating an RPC connection through message ports, you have to consider the following:
- Each type of target is different and has a specific way to establish connections, handle lifecycles, and send/receive messages.
- You may need to wait for one or both of the endpoints to load.
- A single target port can potentially receive connections and messages from multiple sources. For example, a
window
object can receive messages from multiple iframes (some might even be out of your control). To make sure that your RPC messages are not mixed with other messages, you can use thetransportId
option to ensure that only messages that match that specific ID are handled. - For advanced use cases, you can use the
filter
option to filter messages dynamically. The filter function will be called with the rawMessageEvent
object, and should returntrue
if the message should be handled, andfalse
otherwise.