Skip to content

Latest commit

 

History

History
366 lines (266 loc) · 13.4 KB

2-built-in-transports.md

File metadata and controls

366 lines (266 loc) · 13.4 KB

Built-in transports

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.

Table of contents

Iframes

API

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>;

Description

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:

  1. 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.
  2. 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.
  3. 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.

Example

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,
      // ...
    });
    // ...
  },
);

Browser extensions

API

function createTransportFromBrowserRuntimePort(
  port: Browser.Runtime.Port | Chrome.runtime.Port,
  options?: {
    transportId?: string | number;
    filter?: (message: any, port: Browser.Runtime.Port) => boolean;
  },
): RPCTransport;

Description

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.

Example

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),
      // ...
    });
    // ...
  }
});

Workers

API

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;

Description

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.

Example

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(),
  // ...
});
// ...

Broadcast channels

API

export function createTransportFromBroadcastChannel(
  channel: BroadcastChannel,
  options?: {
    transportId?: string | number;
    filter?: (event: MessageEvent) => boolean;
  },
): RPCTransport;

Description

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.

Example

import { createTransportFromBroadcastChannel } from "rpc-anywhere";

const channel = new BroadcastChannel("my-channel");

const rpc = createRPC<Schema>({
  transport: createTransportFromBroadcastChannel(channel),
  // ...
});
// ...

Message ports: windows, workers, broadcast channels

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 and createIframeParentTransport.
  • For workers, you can use createWorkerTransport and createWorkerParentTransport.
  • For broadcast channels, you can use createTransportFromBroadcastChannel.

API

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;

Description

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 the contentWindow 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 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 raw MessageEvent object, and should return true if the message should be handled, and false otherwise.