@mhanzelka/frame-bridge
frame-bridge
Core library. Works in any JS/TS project. No runtime dependencies — only browser APIs.
createBridge<T>(options)
Creates a bridge instance. The generic T defines the message type — it's enforced on both send() and onMessage().
import { createBridge } from "@mhanzelka/frame-bridge";
type AppMessage =
| { type: "ping"; value: number }
| { type: "pong"; value: number };
const bridge = createBridge<AppMessage>({
channelName: "my-channel",
role: "parent",
enabled: ["broadcast-channel"],
});
await bridge.open();| Prop / Method | Type | Default | Description |
|---|---|---|---|
| channelName | string | — | Must match on both sides. |
| role | "parent" | "child" | — | Each side picks a role. For MessageChannel, parent initiates. |
| enabled | TransportType[] | — | Which transports to use: "broadcast-channel", "post-message-channel", "message-channel". |
| targetOrigin | string | — | Required for "post-message-channel". Use "same-origin" for same-origin targets. |
| target | Window | — | Target window for "post-message-channel". |
| prefix | string | — | Optional prefix for the generated bridge ID. |
| options.resolveMessageKey | (msg) => string | — | Custom key used in timeout error messages. |
Bridge<T> methods
| Prop / Method | Type | Default | Description |
|---|---|---|---|
| open() | Promise<void> | — | Open all enabled transports. |
| close() | void | — | Close all transports, reject pending requests. |
| send(data, options?) | Promise<T> | — | Send a request and await the response. |
| sendEvent(data) | void | — | Fire-and-forget — no response expected. |
| onMessage(handler) | () => void | — | Register incoming message handler. Returns unsubscribe function. |
| isOpen() | boolean | — | True if at least one transport is open. |
| active() | BridgeActiveTransports | — | Lists currently enabled and open transports. |
| enable(type) | Promise<void> | — | Enable a transport at runtime. |
| disable(type) | void | — | Disable a transport at runtime. |
| setTarget(win, origin) | void | — | Update the postMessage target window and origin. |
| addMessageObserver(fn) | () => void | — | Subscribe to all sent/received messages for debugging. |
| state | BridgeStateStore | — | Reactive state store, compatible with useSyncExternalStore. |
Send options
const reply = await bridge.send(data, {
timeout: 5000, // ms — throws if no reply (default: 10 000)
signal: abortController.signal, // AbortSignal to cancel
transfer: [arrayBuffer], // Transferable objects
preferredTransport: "message-channel", // hint which transport to use
});Events and handlers
// Fire-and-forget — no reply expected
bridge.sendEvent({ type: "user-activity", timestamp: Date.now() });
// Register handler for incoming messages
const unsubscribe = bridge.onMessage(async (msg, source) => {
if (msg.type === "ping") return { type: "pong", value: msg.value + 1 };
});
// Remove handler later
unsubscribe();Cross-origin iframe
Use post-message-channel for cross-origin targets. Both sides must specify each other's origin.
// parent.ts — host page
const parent = createBridge({
channelName: "iframe-channel",
enabled: ["post-message-channel"],
role: "parent",
targetOrigin: "https://child.example.com",
target: iframeElement.contentWindow,
});
await parent.open();
// child.ts — inside the iframe at child.example.com
const child = createBridge({
channelName: "iframe-channel",
enabled: ["post-message-channel"],
role: "child",
targetOrigin: "https://parent.example.com",
target: window.parent,
});
await child.open();
child.onMessage(async (msg) => ({ ...msg, received: true }));Never use
targetOrigin: "*" in production — it skips origin validation on receive.Popup windows
import { openBridgeWindow } from "@mhanzelka/frame-bridge/bridge/BridgeUtils";
await openBridgeWindow({
url: "/popup",
name: "my-popup",
onMessage: async (msg) => ({ ...msg, handled: true }),
onBridgeReady: async (bridge) => {
await bridge.send({ type: "init" });
},
});Debug logging
import { enableBridgeDebug, disableBridgeDebug } from "@mhanzelka/frame-bridge";
enableBridgeDebug(); // prints all bridge activity to console
disableBridgeDebug(); // silence (default)