SkillAgentSearch skills...

Penpal

Penpal simplifies communication with iframes, workers, and windows by using promise-based methods on top of postMessage.

Install / Use

/learn @Aaronius/Penpal

README

<div align="center"> <picture> <source media="(prefers-color-scheme: dark)" srcset=".github/assets/readme-banner-white.png"> <source media="(prefers-color-scheme: light)" srcset=".github/assets/readme-banner-black.png"> <img alt="Penpal Logo" src=".github/assets/readme-banner-black.png"> </picture> </div> <p align="center"> Penpal simplifies communication with iframes, workers, and windows by <br/> using promise-based methods on top of postMessage. <br/> </p> <div align="center">

npm version

</div>

Migration instructions for each major release can be found on the corresponding GitHub release tag. If you are migrating to v7, see the v7 release tag for migration instructions.

Installation

Using npm

Install Penpal from npm as follows:

npm install penpal

Using a CDN

Alternatively, load a build of Penpal that is already hosted on a CDN:

<script src="https://unpkg.com/penpal@^7/dist/penpal.min.js"></script>

Penpal will then be installed on window.Penpal. Usage is similar to if you were using it from npm, which is documented below, but instead of importing each module, you would access it on the Penpal global variable instead.

Usage with an Iframe

<details> <summary>Expand Details</summary>

Parent Window

import { WindowMessenger, connect } from 'penpal';

const iframe = document.createElement('iframe');
iframe.src = 'https://childorigin.example.com/path/to/iframe.html';
document.body.appendChild(iframe);

const messenger = new WindowMessenger({
  remoteWindow: iframe.contentWindow,
  // Defaults to the current origin.
  allowedOrigins: ['https://childorigin.example.com'],
  // Alternatively,
  // allowedOrigins: [new Url(iframe.src).origin]
});

const connection = connect({
  messenger,
  // Methods the parent window is exposing to the iframe window.
  methods: {
    add(num1, num2) {
      return num1 + num2;
    },
  },
});

const remote = await connection.promise;
// Calling a remote method will always return a promise.
const multiplicationResult = await remote.multiply(2, 6);
console.log(multiplicationResult); // 12
const divisionResult = await remote.divide(12, 4);
console.log(divisionResult); // 3

Iframe Window

import { WindowMessenger, connect } from 'penpal';

const messenger = new WindowMessenger({
  remoteWindow: window.parent,
  // Defaults to the current origin.
  allowedOrigins: ['https://parentorigin.example.com'],
});

const connection = connect({
  messenger,
  // Methods the iframe window is exposing to the parent window.
  methods: {
    multiply(num1, num2) {
      return num1 * num2;
    },
    divide(num1, num2) {
      // Return a promise if asynchronous processing is needed.
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(num1 / num2);
        }, 1000);
      });
    },
  },
});

const remote = await connection.promise;
// Calling a remote method will always return a promise.
const additionResult = await remote.add(2, 6);
console.log(additionResult); // 8
</details>

Usage with an Opened Window

<details> <summary>Expand Details</summary>

Parent Window

import { WindowMessenger, connect } from 'penpal';

const windowUrl = 'https://childorigin.example.com/path/to/window.html';
const childWindow = window.open(windowUrl);

const messenger = new WindowMessenger({
  remoteWindow: childWindow,
  // Defaults to the current origin.
  allowedOrigins: ['https://childorigin.example.com'],
  // Alternatively,
  // allowedOrigins: [new Url(windowUrl).origin]
});

const connection = connect({
  messenger,
  // Methods the parent window is exposing to the child window.
  methods: {
    add(num1, num2) {
      return num1 + num2;
    },
  },
});

const remote = await connection.promise;
// Calling a remote method will always return a promise.
const multiplicationResult = await remote.multiply(2, 6);
console.log(multiplicationResult); // 12
const divisionResult = await remote.divide(12, 4);
console.log(divisionResult); // 3

Opened Window

import { WindowMessenger, connect } from 'penpal';

const messenger = new WindowMessenger({
  remoteWindow: window.opener,
  // Defaults to the current origin.
  allowedOrigins: ['https://parentorigin.example.com'],
});

const connection = connect({
  messenger,
  // Methods the child window is exposing to the parent (opener) window.
  methods: {
    multiply(num1, num2) {
      return num1 * num2;
    },
    divide(num1, num2) {
      // Return a promise if asynchronous processing is needed.
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(num1 / num2);
        }, 1000);
      });
    },
  },
});

const remote = await connection.promise;
// Calling a remote method will always return a promise.
const additionResult = await remote.add(2, 6);
console.log(additionResult); // 8
</details>

Usage with a Worker

<details> <summary>Expand Details</summary>

Window

import { WorkerMessenger, connect } from 'penpal';

const worker = new Worker('worker.js');

const messenger = new WorkerMessenger({
  worker,
});

const connection = connect({
  messenger,
  // Methods the window is exposing to the worker.
  methods: {
    add(num1, num2) {
      return num1 + num2;
    },
  },
});

const remote = await connection.promise;
// Calling a remote method will always return a promise.
const multiplicationResult = await remote.multiply(2, 6);
console.log(multiplicationResult); // 12
const divisionResult = await remote.divide(12, 4);
console.log(divisionResult); // 3

Worker

import { WorkerMessenger, connect } from 'penpal';

const messenger = new WorkerMessenger({
  worker: self,
});

const connection = connect({
  messenger,
  // Methods the worker is exposing to the window.
  methods: {
    multiply(num1, num2) {
      return num1 * num2;
    },
    divide(num1, num2) {
      // Return a promise if asynchronous processing is needed.
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(num1 / num2);
        }, 1000);
      });
    },
  },
});

const remote = await connection.promise;
// Calling a remote method will always return a promise.
const additionResult = await remote.add(2, 6);
console.log(additionResult); // 8
</details>

Usage with a Shared Worker

<details> <summary>Expand Details</summary>

Window

import { PortMessenger, connect } from 'penpal';

const worker = new SharedWorker('shared-worker.js');

const messenger = new PortMessenger({
  port: worker.port,
});

const connection = connect({
  messenger,
  // Methods the window is exposing to the worker.
  methods: {
    add(num1, num2) {
      return num1 + num2;
    },
  },
});

const remote = await connection.promise;
// Calling a remote method will always return a promise.
const multiplicationResult = await remote.multiply(2, 6);
console.log(multiplicationResult); // 12
const divisionResult = await remote.divide(12, 4);
console.log(divisionResult); // 3

Shared Worker

import { PortMessenger, connect } from 'penpal';

self.addEventListener('connect', async (event) => {
  const [port] = event.ports;

  const messenger = new PortMessenger({
    port,
  });

  const connection = connect({
    messenger,
    // Methods the worker is exposing to the window.
    methods: {
      multiply(num1, num2) {
        return num1 * num2;
      },
      divide(num1, num2) {
        // Return a promise if asynchronous processing is needed.
        return new Promise((resolve) => {
          setTimeout(() => {
            resolve(num1 / num2);
          }, 1000);
        });
      },
    },
  });

  const remote = await connection.promise;
  // Calling a remote method will always return a promise.
  const additionResult = await remote.add(2, 6);
  console.log(additionResult); // 8
});
</details>

Usage with a Service Worker

<details> <summary>Expand Details</summary>

Window

import { PortMessenger, connect } from 'penpal';

const initPenpal = async () => {
  const { port1, port2 } = new MessageChannel();

  navigator.serviceWorker.controller?.postMessage(
    {
      type: 'INIT_PENPAL',
      port: port2,
    },
    {
      transfer: [port2],
    }
  );

  const messenger = new PortMessenger({
    port: port1,
  });

  const connection = connect({
    messenger,
    // Methods the window is exposing to the worker.
    methods: {
      add(num1, num2) {
        return num1 + num2;
      },
    },
  });

  const remote = await connection.promise;
  // Calling a remote method will always return a promise.
  const multiplicationResult = await remote.multiply(2, 6);
  console.log(multiplicationResult); // 12
  const divisionResult = await remote.divide(12, 4);
  console.log(divisionResult); // 3
};

if (navigator.serviceWorker.controller) {
  initPenpal();
}

navigator.serviceWorker.addEventListener('controllerchange', initPenpal);
navigator.serviceWorker.register('service-worker.js');

Service Worker

import { PortMessenger, connect } from 'penpal';

self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', () => self.clients.claim());
self.addEventListener('message', async (event) => {
  if (event.data?.type !== 'INIT_PENPAL') {
    return;
  }

  const { port } = event.data;

  const messenger = new PortMessenger({
    port,
  });

  const connection = connect({
    messenger,
    // Methods worker is exposing to window.
    methods: {
      multiply(num1, num2) {
        return num1 * num2;
      },
      divide(num1, num2) {
        // Return a promise if asynchronous processing is needed.
        return new Promise((resolve) => {
          setTimeout(() => {
        
View on GitHub
GitHub Stars529
CategoryDevelopment
Updated3h ago
Forks65

Languages

TypeScript

Security Score

100/100

Audited on Mar 29, 2026

No findings