Penpal
Penpal simplifies communication with iframes, workers, and windows by using promise-based methods on top of postMessage.
Install / Use
/learn @Aaronius/PenpalREADME
<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">
</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(() => {
