npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

frame-link

v4.0.1

Published

async two way communication with iframe over postMessage

Downloads

520

Readme

frame-link

Type-safe, bidirectional postMessage communication between a parent page and an iframe. Handles the handshake, request/response pairing, and timeout — you just define your message types and call send.

React? See frame-link-react for hooks that wrap this core library.


Install

Install on both the parent page and the iframe:

npm install frame-link
# or
yarn add frame-link

Quickstart

Parent page

import { createFrameLink } from "frame-link";

const iframe = document.getElementById("my-iframe") as HTMLIFrameElement;

// Always use an explicit origin — never "*" in production.
const link = createFrameLink({ targetOrigin: "https://app.example.com" });

// Wait for the handshake to complete before sending.
await link.connect(iframe.contentWindow!);

// Send a request and await the response.
const result = await link.send("user:get", { id: "123" });
console.log(result.name);

Iframe

import { createFrameLink } from "frame-link";

const link = createFrameLink({ targetOrigin: "https://host.example.com" });

// Register a handler before connecting.
link.on("user:get", async ({ id }) => {
  const user = await db.getUser(id);
  return { name: user.name, email: user.email };
});

// Connect back to the parent window.
await link.connect(window.parent);

Both sides must call connect. The handshake is symmetric: whichever side calls connect first waits until the other side responds.


Core concepts

Creating a link

createFrameLink(options) returns a FrameLink instance. Options:

| Option | Type | Default | Description | |---|---|---|---| | targetOrigin | string | — | Origin of the remote window. Use an exact origin in production. | | timeout | number | 5000 | Milliseconds before send or connect rejects. |

Request / response via send

link.send(key, payload) sends a message and returns a Promise that resolves to the response. The remote side handles it with link.on(key, handler), where the handler's return value becomes the response.

// Sender
const response = await link.send("user:update", { id: "42", name: "Ada" });
console.log(response.success); // boolean

// Receiver
link.on("user:update", async ({ id, name }) => {
  const ok = await db.updateUser(id, name);
  return { success: ok };
});

Events (fire-and-forget)

Define the response type as void in your registry for events where the sender does not wait for a meaningful reply:

link.on("notification:show", ({ message, type }) => {
  showToast(message, type);
  // No return value needed — response is void.
});

// Fire and forget — still returns Promise<void>, which you can discard.
void link.send("notification:show", { message: "Saved!", type: "info" });

Handshake / connection status via connect and connected

link.connect(targetWindow) performs a handshake and returns a Promise<void> that resolves once both sides are ready. After that, link.connected is true.

await link.connect(iframe.contentWindow!);
console.log(link.connected); // true

link.destroy(); // tears down listeners and resets state
console.log(link.connected); // false

Always await link.connect(...) before calling link.send(...). Sending before the handshake completes throws an error.


Typed message registry

Define your messages once as an interface that extends MessageRegistry. Each key maps to a MessageDefinition<Payload, Response>.

import { createFrameLink } from "frame-link";
import type { MessageDefinition, MessageRegistry } from "frame-link";

// Define a registry — do this in a shared file imported by both sides.
interface SampleMessages extends MessageRegistry {
  /** Fetch a user by ID. */
  "user:get": MessageDefinition<{ id: string }, { name: string; email: string }>;

  /** Update a user's display name. */
  "user:update": MessageDefinition<{ id: string; name: string }, { success: boolean }>;

  /**
   * Fire-and-forget notification event.
   * Response is void — the sender does not wait for a reply.
   */
  "notification:show": MessageDefinition<
    { message: string; type: "info" | "error" },
    void
  >;
}

// Pass the registry as a type parameter — both sides must use the same registry.
const link = createFrameLink<SampleMessages>({ targetOrigin: "https://app.example.com" });

// The compiler knows the exact payload and response shapes for every key.
const user = await link.send("user:get", { id: "123" });
//    ^ { name: string; email: string }

void link.send("notification:show", { message: "Done!", type: "info" });
//                                   ^ { message: string; type: "info" | "error" }

Utility types

import type { PayloadOf, ResponseOf } from "frame-link";

type GetPayload  = PayloadOf<SampleMessages, "user:get">;
// => { id: string }

type GetResponse = ResponseOf<SampleMessages, "user:get">;
// => { name: string; email: string }

Security

Never use targetOrigin: "*" outside a local demo

Passing "*" as targetOrigin tells the browser to deliver your postMessage to any origin. This can leak sensitive data (tokens, PII, internal state) to a malicious page that has embedded your iframe or that your page has embedded.

Rule: Always set targetOrigin to the exact origin of the remote window in every non-demo environment.

// WRONG — do not use in production
const link = createFrameLink({ targetOrigin: "*" }); // leaks messages to any page

// CORRECT — lock it to the exact origin of the remote window
const link = createFrameLink({ targetOrigin: "https://app.example.com" });

The * wildcard is acceptable only in local development where both sides run on localhost and you accept the risk, and it should be behind a build flag (e.g., process.env.NODE_ENV === "development").

Receiver-side origin validation

frame-link validates the sender's origin on every incoming message using an internal createOriginValidator. When targetOrigin is an exact origin string, any message arriving from a different origin is silently dropped — it is never delivered to your handlers. This means both sides of the link reject unexpected senders automatically, as long as you supply an explicit targetOrigin.


API reference

createFrameLink<TRegistry>(options): FrameLink<TRegistry>

Creates a new FrameLink instance.

function createFrameLink<TRegistry extends MessageRegistry>(
  options: FrameLinkOptions
): FrameLink<TRegistry>

FrameLinkOptions

interface FrameLinkOptions {
  targetOrigin: string; // Origin of the remote window (e.g. "https://app.example.com")
  timeout?: number;     // Request timeout in ms. Default: 5000
}

FrameLink<TRegistry> instance

link.connect(target: Window): Promise<void>

Performs the handshake with the remote window. Must be awaited before calling send. Rejects if the handshake is not completed within timeout ms.

await link.connect(iframe.contentWindow!); // parent side
await link.connect(window.parent);         // iframe side

link.connected: boolean (read-only)

true after the handshake completes; false before connect resolves or after destroy is called.

link.send<TKey>(key: TKey, payload: PayloadOf<TRegistry, TKey>): Promise<ResponseOf<TRegistry, TKey>>

Sends a message and waits for a response. Rejects after timeout ms if no response is received.

const user = await link.send("user:get", { id: "123" });

link.on<TKey>(key: TKey, handler: MessageHandler<TRegistry, TKey>): () => void

Registers a handler for incoming messages of type key. Returns an unsubscribe function.

const unsubscribe = link.on("user:get", async ({ id }) => {
  return { name: "Ada", email: "[email protected]" };
});

unsubscribe(); // remove the handler

MessageHandler signature:

type MessageHandler<TRegistry, TKey> = (
  payload: PayloadOf<TRegistry, TKey>
) => ResponseOf<TRegistry, TKey> | Promise<ResponseOf<TRegistry, TKey>>;

link.off(key: keyof TRegistry & string): void

Removes the handler for key.

link.destroy(): void

Tears down all internal listeners and resets connection state. Call this in cleanup (e.g., component unmount, page unload).


Type exports

| Export | Kind | Description | |---|---|---| | createFrameLink | function | Creates a FrameLink instance | | isWireMessage | function | Type guard — returns true if the value is a valid WireMessage | | FrameLink | type | Interface for a FrameLink instance | | FrameLinkOptions | type | Options passed to createFrameLink | | MessageDefinition | type | Defines a message's payload and response types | | MessageHandler | type | Handler function type for a message key | | MessageRegistry | type | A record mapping message keys to MessageDefinition | | PayloadOf | type | Extracts the payload type for a key from a registry | | ResponseOf | type | Extracts the response type for a key from a registry | | WireMessage | type | Internal protocol message type |


Troubleshooting

Messages are silently dropped (origin mismatch)

Symptom: link.send(...) times out; handlers never fire.

Cause: The targetOrigin you set does not match the actual origin of the remote window. frame-link drops all messages from unexpected origins.

Fix: Make sure targetOrigin on the sender matches the window.location.origin of the receiver, and vice versa.

Parent uses  targetOrigin: "https://app.example.com"
↓  must match  ↓
Iframe is served from  https://app.example.com

Handshake never completes (connect hangs or rejects with timeout)

Symptom: await link.connect(...) hangs and eventually rejects.

Causes and fixes:

  • One side did not call connect — both the parent and the iframe must call connect.
  • The iframe is not yet loaded when the parent calls connect — wait for the iframe's load event before calling connect on the parent side.
  • targetOrigin mismatch (see above) prevents the handshake messages from being delivered.
  • The timeout option is too short for your network/load conditions — increase it.
iframe.addEventListener("load", async () => {
  await link.connect(iframe.contentWindow!);
  // safe to send now
});

Error: "Not connected to target window"

Symptom: link.send(...) throws immediately.

Cause: send was called before connect resolved (or connect was never called).

Fix: Always await link.connect(target) before calling link.send(...).

// Wrong
link.send("user:get", { id: "1" }); // throws — not connected yet

// Correct
await link.connect(iframe.contentWindow!);
const user = await link.send("user:get", { id: "1" });