Developer SDK

JavaScript examples

Install the SDK · View source on GitHub ↗

integration/companion.mjs

// The opaque transport reaches a full local provider. Production browser apps
// can supply the same SDK transport contract from their native extension.
export function companionTransport(url, token, WebSocketClass = globalThis.WebSocket) {
  const endpoint = new URL(url);
  if (!["ws:", "wss:"].includes(endpoint.protocol) || !["127.0.0.1", "[::1]"].includes(endpoint.hostname) || endpoint.username || endpoint.password) {
    throw new Error("The example companion URL must use ws(s) and a numeric loopback host");
  }
  if (typeof token !== "string" || token.length < 32) throw new Error("Set URNETWORK_COMPANION_TOKEN to the companion's random token");
  endpoint.searchParams.set("token", token);
  return {open(callbacks) {
    const ws = new WebSocketClass(endpoint.href);
    ws.binaryType = "arraybuffer";
    let closed = false;
    const opened = () => callbacks.opened();
    const message = event => {
      if (closed) return;
      if (!(event.data instanceof ArrayBuffer)) { terminate("Companion sent a non-binary frame"); return; }
      callbacks.message(new Uint8Array(event.data).slice());
    };
    const ended = () => terminate("Companion connection closed");
    const failed = () => terminate("Companion connection failed");
    const terminate = (reason, notify = true) => {
      if (closed) return;
      closed = true;
      ws.removeEventListener("open", opened); ws.removeEventListener("message", message);
      ws.removeEventListener("close", ended); ws.removeEventListener("error", failed);
      ws.close(); if (notify) callbacks.closed(reason);
    };
    ws.addEventListener("open", opened); ws.addEventListener("message", message);
    ws.addEventListener("close", ended); ws.addEventListener("error", failed);
    return {
      send(bytes) {
        if (closed || ws.readyState !== 1) throw new Error("Companion connection is closed");
        if (ws.bufferedAmount + bytes.byteLength > 1024*1024) { terminate("Companion send queue exceeded 1 MiB"); throw new Error("Companion send queue overflow"); }
        ws.send(bytes.slice());
      },
      // SDK-initiated close releases its callback functions synchronously.
      // Do not reenter callbacks.closed or retain socket event listeners.
      close() { terminate("SDK closed companion transport", false); },
    };
  }};
}

export async function openMessageDevice(URNetwork, config, token, wasmOptions = {}) {
  for (const key of ["apiUrl", "platformUrl", "byJwt", "instanceId", "companionUrl"]) {
    if (typeof config[key] !== "string" || !config[key]) throw new Error("Message device configuration requires " + key);
  }
  const transport = companionTransport(config.companionUrl, token);
  const sdk = await URNetwork.init(wasmOptions);
  let device;
  try {
    device = sdk.createExtensionDeviceRemote({...config, transport});
    // Return before first sync so the caller can install peer listeners.
    // Browser listener registration otherwise forces a new RPC generation.
    return {sdk, device, close() {device.close(); sdk.close();}};
  } catch (error) {device?.close(); sdk.close(); throw error;}
}