@nanohq/postmessage-hub
v0.1.0
Published
A comprehensive message routing system for parent-child and sibling iframe communication with TypeScript support
Maintainers
Readme
@nanohq/postmessage-hub
A comprehensive, type-safe message routing system for parent-child and sibling iframe communication. Built with TypeScript for maximum reliability and developer experience.
Features
- ✨ Two-way Communication: Direct parent-child messaging
- 🔄 Sibling Communication: Route messages between iframes through parent
- 🎯 Type-Safe: Full TypeScript support with comprehensive type definitions
- 🔒 Secure: Origin validation and whitelisting
- 📝 Request-Response Pattern: Async/await support for message exchanges
- 📡 Broadcasting: Send messages to all registered iframes
- 🐛 Debug Mode: Built-in logging for development
- ⚡ Lightweight: Minimal dependencies, maximum performance
- 🧪 Well-Tested: Comprehensive test coverage
Installation
npm install @nanohq/postmessage-hubor
yarn add @nanohq/postmessage-hubQuick Start
Parent Window Setup
import { PostMessageHub } from "@nanohq/postmessage-hub";
// Initialize the hub in the parent window
const parentHub = new PostMessageHub({
instanceId: "parent",
debug: true, // Enable debug logging
allowedOrigins: [
"https://iframe1.example.com",
"https://iframe2.example.com",
],
});
// Register iframes
const iframe1 = document.getElementById("iframe-1") as HTMLIFrameElement;
const iframe2 = document.getElementById("iframe-2") as HTMLIFrameElement;
parentHub.registerIframe(
"iframe-1",
iframe1.contentWindow!,
"https://iframe1.example.com"
);
parentHub.registerIframe(
"iframe-2",
iframe2.contentWindow!,
"https://iframe2.example.com"
);
// Listen for messages
parentHub.on("greeting", (data, event) => {
console.log("Received greeting from:", data.source);
console.log("Message:", data.payload);
});
// Send message to specific iframe
parentHub.sendToIframe("iframe-1", {
type: "command",
payload: { action: "refresh" },
});
// Broadcast to all iframes
parentHub.broadcast({
type: "notification",
payload: { message: "System update available" },
});Iframe Setup
import { PostMessageHub } from "@nanohq/postmessage-hub";
// Initialize the hub in the iframe
const iframeHub = new PostMessageHub({
instanceId: "iframe-1",
debug: true,
});
// Connect to parent
iframeHub.connectToParent(window.parent, "https://parent.example.com");
// Listen for messages from parent
iframeHub.on("command", (data, event) => {
console.log("Received command:", data.payload);
});
// Send message to parent
iframeHub.sendToParent({
type: "greeting",
payload: { message: "Hello from iframe-1!" },
});
// Send message to sibling iframe (routed through parent)
iframeHub.sendToSibling("iframe-2", {
type: "peer-message",
payload: { text: "Hello sibling!" },
});API Reference
PostMessageHub
Constructor
new PostMessageHub(config?: PostMessageHubConfig)Config Options:
instanceId?: string- Unique identifier for this hub instancedebug?: boolean- Enable debug logging (default: false)timeout?: number- Timeout for awaiting responses in ms (default: 5000)allowedOrigins?: string[]- Whitelist of allowed originssuppressHandshake?: boolean- Disable initial registration handshake (default: false)deduplication?: boolean- Prevent duplicate messages (default: false)deduplicationTimeout?: number- Time window for deduplication in ms (default: 1000)
Methods
registerIframe(iframeId: string, iframeWindow: Window, origin: string)
Register an iframe for communication (parent only).
parentHub.registerIframe(
"iframe-1",
iframe.contentWindow,
"https://example.com"
);unregisterIframe(iframeId: string)
Unregister an iframe.
parentHub.unregisterIframe("iframe-1");connectToParent(parentWindow: Window, parentOrigin: string)
Connect to parent window (iframe only).
iframeHub.connectToParent(window.parent, "https://parent.com");sendToParent(data: PostMessageData)
Send message to parent (iframe only).
iframeHub.sendToParent({
type: "status",
payload: { ready: true },
});sendToIframe(targetId: string, data: PostMessageData)
Send message to specific iframe (parent only).
parentHub.sendToIframe("iframe-1", {
type: "update",
payload: { data: "new data" },
});sendToSibling(targetId: string, data: PostMessageData)
Send message to sibling iframe (iframe only, routed through parent).
iframeHub.sendToSibling("iframe-2", {
type: "share",
payload: { data: "shared data" },
});broadcast(data: PostMessageData)
Broadcast message to all registered iframes (parent only).
parentHub.broadcast({
type: "announcement",
payload: { message: "Important update" },
});on(type: string, handler: MessageHandler)
Register a message handler for specific type.
hub.on("custom-event", (data, event) => {
console.log("Received custom event:", data.payload);
});
// Wildcard handler for all messages
hub.on("*", (data, event) => {
console.log("Received any message:", data);
});off(type: string, handler: MessageHandler)
Unregister a message handler.
const handler = (data) => console.log(data);
hub.on("event", handler);
hub.off("event", handler);sendAndAwaitResponse(target: string, data: PostMessageData)
Send message and wait for response (Promise-based).
try {
const response = await parentHub.sendAndAwaitResponse("iframe-1", {
type: "query",
payload: { question: "What is your status?" },
});
console.log("Response:", response.payload);
} catch (error) {
console.error("Request timeout or error:", error);
}respond(originalMessage: PostMessageData, responseData: PostMessageData)
Respond to a message.
hub.on("query", (data, event) => {
hub.respond(data, {
type: "answer",
payload: { status: "ready" },
});
});addAllowedOrigin(origin: string)
Add an allowed origin dynamically.
hub.addAllowedOrigin("https://new-trusted-site.com");getRegisteredIframeIds(): string[]
Get list of all registered iframe IDs.
const iframeIds = parentHub.getRegisteredIframeIds();
console.log("Registered iframes:", iframeIds);destroy()
Clean up event listeners and resources.
hub.destroy();Usage Examples
Example 1: Simple Parent-Child Communication
// Parent
const hub = new PostMessageHub({ instanceId: "parent" });
const iframe = document.querySelector("iframe") as HTMLIFrameElement;
hub.registerIframe("child", iframe.contentWindow!, "https://child.com");
hub.on("ready", (data) => {
console.log("Child is ready!");
hub.sendToIframe("child", {
type: "initialize",
payload: { config: { theme: "dark" } },
});
});
// Child
const childHub = new PostMessageHub({ instanceId: "child" });
childHub.connectToParent(window.parent, "https://parent.com");
childHub.sendToParent({
type: "ready",
payload: { version: "1.0.0" },
});
childHub.on("initialize", (data) => {
console.log("Received config:", data.payload.config);
});Example 2: Sibling Communication
// Parent
const hub = new PostMessageHub({ instanceId: "parent", debug: true });
hub.registerIframe(
"sidebar",
sidebarIframe.contentWindow!,
"https://sidebar.com"
);
hub.registerIframe(
"content",
contentIframe.contentWindow!,
"https://content.com"
);
// Sidebar iframe
const sidebarHub = new PostMessageHub({ instanceId: "sidebar" });
sidebarHub.connectToParent(window.parent, "https://parent.com");
// When user clicks navigation
document.querySelector("#nav-item").addEventListener("click", () => {
sidebarHub.sendToSibling("content", {
type: "navigate",
payload: { page: "dashboard" },
});
});
// Content iframe
const contentHub = new PostMessageHub({ instanceId: "content" });
contentHub.connectToParent(window.parent, "https://parent.com");
contentHub.on("navigate", (data) => {
console.log("Navigating to:", data.payload.page);
loadPage(data.payload.page);
});Example 3: Request-Response Pattern
// Parent
const hub = new PostMessageHub({ instanceId: "parent" });
hub.registerIframe("worker", workerIframe.contentWindow!, "https://worker.com");
async function fetchDataFromWorker() {
try {
const response = await hub.sendAndAwaitResponse("worker", {
type: "fetch-data",
payload: { userId: 123 },
});
console.log("Data received:", response.payload.data);
} catch (error) {
console.error("Failed to fetch data:", error);
}
}
// Worker iframe
const workerHub = new PostMessageHub({ instanceId: "worker" });
workerHub.connectToParent(window.parent, "https://parent.com");
workerHub.on("fetch-data", async (data, event) => {
const userData = await fetchUser(data.payload.userId);
workerHub.respond(data, {
type: "data-response",
payload: { data: userData },
});
});Example 4: Broadcasting Updates
// Parent
const hub = new PostMessageHub({ instanceId: "parent" });
hub.registerIframe(
"dashboard",
dashboardIframe.contentWindow!,
"https://dash.com"
);
hub.registerIframe("chart", chartIframe.contentWindow!, "https://chart.com");
hub.registerIframe("table", tableIframe.contentWindow!, "https://table.com");
// Broadcast real-time updates to all iframes
setInterval(() => {
hub.broadcast({
type: "data-update",
payload: {
timestamp: Date.now(),
metrics: fetchLatestMetrics(),
},
});
}, 5000);
// Each iframe listens for updates
const dashHub = new PostMessageHub({ instanceId: "dashboard" });
dashHub.connectToParent(window.parent, "https://parent.com");
dashHub.on("data-update", (data) => {
updateDashboard(data.payload.metrics);
});Example 5: Standalone Child Mode (Drop-in Replacement)
Use this mode when the parent window is NOT using postmessage-hub (vanilla implementation).
// Child Iframe
const hub = new PostMessageHub({
instanceId: "child",
suppressHandshake: true, // 1. Don't send registration handshake
});
hub.connectToParent(window.parent, "*");
// 2. Send raw data that vanilla parent expects
hub.sendToParent(
{
type: "my-event",
payload: { some: "data" },
},
{ raw: true } // Sends just the payload object
);
{ raw: true } // Sends just the payload object
);Example 6: Event Deduplication
Prevent duplicate messages from being sent (e.g., from React useEffect double-firing).
const hub = new PostMessageHub({
instanceId: "child",
deduplication: true, // Enable deduplication
deduplicationTimeout: 1000, // Time window in ms (default: 1000)
});
// This will be sent
hub.sendToParent({ type: "init", payload: { id: 1 } });
// This will be BLOCKED if sent within 1000ms
hub.sendToParent({ type: "init", payload: { id: 1 } });
// This will be sent (different payload)
hub.sendToParent({ type: "init", payload: { id: 2 } });TypeScript Support
Full TypeScript definitions are included:
import {
PostMessageHub,
PostMessageData,
PostMessageHubConfig,
MessageType,
ConnectionStatus,
HubError,
} from "@nanohq/postmessage-hub";
// Type-safe message data
const message: PostMessageData = {
type: "custom",
payload: { value: 42 },
source: "iframe-1",
target: "iframe-2",
};
// Type-safe config
const config: PostMessageHubConfig = {
instanceId: "my-hub",
debug: true,
timeout: 10000,
allowedOrigins: ["https://example.com"],
};Security Best Practices
- Always specify allowed origins in production:
const hub = new PostMessageHub({
allowedOrigins: ["https://trusted-domain.com"],
});- Validate message payloads before processing:
hub.on("sensitive-action", (data) => {
if (!isValidPayload(data.payload)) {
console.error("Invalid payload received");
return;
}
processSensitiveAction(data.payload);
});Use HTTPS for all iframe sources in production.
Avoid sending sensitive data directly in messages; use tokens or identifiers instead.
Browser Support
- Chrome/Edge: Latest 2 versions
- Firefox: Latest 2 versions
- Safari: Latest 2 versions
- All browsers supporting
postMessageAPI
Development
# Install dependencies
npm install
# Build
npm run build
# Run tests
npm test
# Watch mode
npm run dev
# Run tests with coverage
npm run test:coverageContributing
Contributions are welcome! Please read our contributing guidelines before submitting PRs.
License
MIT © Nano Team
Changelog
See CHANGELOG.md for version history.
Support
- 📧 Email: [email protected]
- 🐛 Issues: GitHub Issues
- 📖 Docs: Full Documentation
