Documentation Index
Fetch the complete documentation index at: https://mintlify.com/ading2210/sandstone/llms.txt
Use this file to discover all available pages before exploring further.
The RPC module handles all communication between the host page and the sandboxed iframe. It uses a MessageChannel to create a private, dedicated MessagePort pair for each frame, then wraps postMessage calls in a simple request/reply protocol so that either side can call named procedures on the other and receive a result back as a Promise. This page documents the internal API — you need it only if you are extending Sandstone or building tooling on top of it. If you are embedding Sandstone in a page, the ProxyFrame class is the correct entry point.
This is an internals reference. Typical users interact with Sandstone exclusively through ProxyFrame. The exports described here are subject to change across minor versions.
RPCTarget
RPCTarget is a lightweight wrapper around a MessagePort or Window that normalises message listener management and forwards incoming messages to a single onmessage callback.
import { RPCTarget } from "./rpc.mjs";
const target = new RPCTarget();
target.onmessage = (event, rpcTarget) => { /* handle message */ };
target.set_target(someMessagePort);
Constructor
new RPCTarget(target = null)
If you pass an initial target, set_target is called immediately.
Methods
| Method | Description |
|---|
set_target(target) | Replace the current underlying target. Removes the message listener from the old target and all extra targets, then attaches the listener to the new target. extra_targets is reset to an empty array. |
add_extra_target(target) | Attach the message listener to an additional port or window without replacing the primary target. Useful when a new MessagePort arrives via an "attach" message. |
postMessage(...args) | Forward a postMessage call to the primary target. |
Properties
| Property | Type | Description |
|---|
target | MessagePort | Window | null | The primary message target. |
extra_targets | Array | Additional ports attached via add_extra_target. |
onmessage | Function | Callback invoked for every incoming message. Receives (event, rpcTarget). Defaults to a no-op. |
rpc_handlers
// Object mapping procedure names to handler functions
export const rpc_handlers = {}
A plain object that maps procedure names to their handler functions. To register a handler, assign directly to this object:
import { rpc_handlers } from "./rpc.mjs";
rpc_handlers["my_procedure"] = async (arg1, arg2) => {
return arg1 + arg2;
};
When a "procedure" message arrives, message_listener looks up the procedure name in rpc_handlers, calls the function with the message arguments, and sends a "reply" message back to the caller. If the handler throws, the reply carries the error and the caller’s Promise is rejected.
call_procedure
// Returns a Promise that resolves with the remote handler's return value
async function call_procedure(target, procedure, args)
Send a "procedure" message to target and return a Promise that resolves with the remote handler’s return value, or rejects if the handler throws.
| Parameter | Type | Description |
|---|
target | RPCTarget | MessagePort | HTMLIFrameElement | The destination. If an HTMLIFrameElement is passed, contentWindow is used automatically. |
procedure | string | The name of the procedure to call. Must match a key in the remote side’s rpc_handlers. |
args | Array | Arguments forwarded to the remote handler. |
const result = await call_procedure(frameRpcTarget, "eval", ["document.title"]);
create_rpc_wrapper
// Returns a function that calls call_procedure(target, procedure, [...arguments])
function create_rpc_wrapper(target, procedure)
Return a function that calls call_procedure(target, procedure, [...arguments]). This is the standard way to create a strongly-typed shortcut for a frequently-called remote procedure.
const send_page = create_rpc_wrapper(this.rpc_target, "html");
// Later:
await send_page({ url, html, frame_id, /* ... */ });
ProxyFrame uses this pattern internally to create send_page, get_favicon, and eval_js:
this.send_page = rpc.create_rpc_wrapper(this.rpc_target, "html");
this.get_favicon = rpc.create_rpc_wrapper(this.rpc_target, "favicon");
this.eval_js = rpc.create_rpc_wrapper(this.rpc_target, "eval");
message_listener
async function message_listener(event, target)
The central message handler for the RPC layer. Attach it to an RPCTarget.onmessage or directly to window.addEventListener("message", ...). It handles three message types:
msg.type | Behaviour |
|---|
"procedure" | Look up the procedure in rpc_handlers, call it, and postMessage a "reply" back to the source. |
"reply" | Resolve or reject the pending Promise stored in rpc_requests for the matching msg.id. |
"attach" | If the current role is "frame", forward the transferred MessagePort to the host. If the current role is "host", call add_extra_target on the port so it becomes an additional listener. |
set_role
// value: "host" or "frame"
function set_role(value)
Set whether the current execution context is the host page or the sandboxed frame. The value is used by message_listener to decide how to handle "attach" messages.
set_host
function set_host(frame, msg_port)
Send a "set_host" message to a child frame, transferring msg_port to it. The frame’s host_set_handler receives this message and calls host.set_target(msg_port) so that subsequent RPC calls from the frame travel over the MessagePort rather than over postMessage to the parent window.
wait_on_frame
async function wait_on_frame(frame): Promise<void>
Repeatedly send "ping" messages to a frame’s contentWindow every 50 ms until a "pong" message is received. This ensures the frame’s JavaScript runtime has fully initialised before the host attempts to establish the MessageChannel.
attach_host
function attach_host(msg_port)
Send an "attach" message to the host, transferring msg_port. The host adds the port as an extra target on its RPCTarget, allowing additional workers or nested frames to communicate directly with the host.
How host navigation works
The following is the real sequence inside ProxyFrame.navigate_to in controller.mjs:
// 1. Wait for the frame's JS runtime to become responsive
await rpc.wait_on_frame(this.iframe);
// 2. Create a dedicated MessageChannel for this navigation
let msg_channel = new MessageChannel();
this.rpc_target.set_target(msg_channel.port1);
msg_channel.port1.start();
// 3. Transfer port2 to the frame so it knows how to reach the host
await rpc.set_host(this.iframe, msg_channel.port2);
// 4. Send the fetched HTML (and metadata) to the frame over the channel
await this.send_page({
url: this.url.href,
html: html,
frame_id: this.id,
settings: settings,
default_settings: this.default_settings,
local_storage: local_storage[this.url.origin],
version: version
});
this.send_page is a wrapper created by create_rpc_wrapper(this.rpc_target, "html"). On the frame side, the "html" handler is load_html in loader.mjs.
Message type reference
type | Direction | Payload | Description |
|---|
"procedure" | either | { id, procedure, arguments } | Call a named procedure. Expects a "reply". |
"reply" | either | { id, content: { value, success } } | Response to a "procedure" message. |
"attach" | frame → host | transferable MessagePort | Register an additional port with the host. |
"set_host" | host → frame | transferable MessagePort | Tell the frame which port to use as its host target. |
"ping" | host → frame | { id } | Liveness probe sent before establishing the MessageChannel. |
"pong" | frame → host | { id } | Liveness reply; signals the frame is ready. |