How to Implement Cross-Window Communication in JavaScript
Web applications frequently work with multiple browsing contexts. A page might embed third-party widgets in iframes, open popups for authentication flows, or coordinate behavior across multiple tabs. These contexts often need to exchange data, but the browser's security model places strict boundaries on what one window can access in another.
Understanding how the Same-Origin Policy governs cross-window access, how postMessage enables safe communication across origins, how to properly validate incoming messages, and how iframes interact with their parent pages gives you the tools to build secure, multi-context applications. This guide covers each of these topics with practical examples and security best practices.
Same-Origin Policy
The Same-Origin Policy is the browser's fundamental security mechanism for isolating browsing contexts from each other. It determines what one window (or iframe) can access in another based on their origins.
What Is an Origin?
An origin is defined by three components: protocol, hostname, and port. Two URLs share the same origin only if all three match exactly.
| URL A | URL B | Same Origin? | Reason |
|---|---|---|---|
https://site.com/page1 | https://site.com/page2 | Yes | Same protocol, host, port |
https://site.com | http://site.com | No | Different protocol |
https://site.com | https://www.site.com | No | Different hostname |
https://site.com | https://site.com:8080 | No | Different port |
https://site.com:443 | https://site.com | Yes | 443 is the default HTTPS port |
What Same-Origin Allows
When two windows share the same origin, they have full mutual access. Each can read and write the other's DOM, variables, and call functions:
// Parent page at https://mysite.com/parent.html
const popup = window.open('https://mysite.com/child.html', 'child');
popup.addEventListener('load', () => {
// Full access: read DOM
console.log(popup.document.title);
// Full access: modify DOM
popup.document.body.style.background = 'lightyellow';
// Full access: call functions
popup.someFunction('data');
// Full access: read variables
console.log(popup.someVariable);
});
// Child page at https://mysite.com/child.html
// Full access back to the opener
console.log(window.opener.document.title);
window.opener.handleResult({ success: true });
What Cross-Origin Blocks
When two windows are on different origins, the browser blocks almost all access. Attempting to read or write the other window's properties throws a DOMException:
// Parent at https://mysite.com
const popup = window.open('https://other-site.com/page.html');
// After popup loads...
try {
console.log(popup.document.title);
} catch (error) {
console.error(error);
// DOMException: Blocked a frame with origin "https://mysite.com"
// from accessing a cross-origin frame.
}
try {
console.log(popup.location.href);
} catch (error) {
// Also blocked: cannot read the full URL
}
The Few Cross-Origin Properties That ARE Accessible
Even across origins, a few properties remain accessible for basic window management:
Readable and writable:
window.location(write only: you can navigate the window but not read its URL)window.closedwindow.frameswindow.lengthwindow.openerwindow.parentwindow.topwindow.self
Methods:
window.close()window.focus()window.blur()window.postMessage()
// Cross-origin: these work
const popup = window.open('https://other-site.com');
console.log(popup.closed); // false (readable)
popup.close(); // Works (can close the popup)
popup.focus(); // Works (can bring to front)
popup.postMessage('hello', 'https://other-site.com'); // Works (messaging)
// Cross-origin: can SET location (navigate) but NOT READ it
popup.location = 'https://other-site.com/new-page'; // Works
console.log(popup.location.href); // Throws (cannot read)
document.domain: Legacy Same-Origin Relaxation
Historically, pages on subdomains could relax the same-origin policy by setting document.domain to a common parent domain:
// On page1.example.com
document.domain = 'example.com';
// On page2.example.com
document.domain = 'example.com';
// Now they can access each other as if same-origin
document.domain is deprecated and being removed from browsers. Chrome has already disabled it by default. Do not use it in new code. Use postMessage for cross-subdomain communication instead.
window.postMessage(): Cross-Origin Communication
The postMessage API is the standard, secure way to communicate between windows (or iframes) that may be on different origins. It provides a controlled channel where each side explicitly sends messages and the receiver explicitly validates where messages come from.
How postMessage Works
The sending window calls postMessage() on the receiving window's reference, specifying the data and the expected origin of the receiver:
targetWindow.postMessage(data, targetOrigin);
data: Any value that can be cloned using the structured clone algorithm (strings, numbers, objects, arrays,Map,Set,ArrayBuffer, etc.). Functions and DOM nodes cannot be sent.targetOrigin: The expected origin of the target window. The message is only delivered if the target window's origin matches. Use'*'to skip the origin check (dangerous for sensitive data).
The receiving window listens for message events:
window.addEventListener('message', (event) => {
console.log('Received:', event.data);
console.log('From origin:', event.origin);
console.log('Source window:', event.source);
});
Basic Example: Opener and Popup
Opener page (https://mysite.com):
const popup = window.open('https://other-site.com/widget', 'widget', 'width=400,height=300');
// Send a message to the popup
// Wait for it to load and set up its listener
setTimeout(() => {
popup.postMessage(
{ action: 'initialize', userId: 12345 },
'https://other-site.com' // Only deliver if popup is on this origin
);
}, 1000);
// Listen for responses
window.addEventListener('message', (event) => {
if (event.origin !== 'https://other-site.com') return;
console.log('Response from popup:', event.data);
});
Popup page (https://other-site.com/widget):
window.addEventListener('message', (event) => {
// Verify the sender's origin
if (event.origin !== 'https://mysite.com') return;
if (event.data.action === 'initialize') {
console.log('Initializing for user:', event.data.userId);
// Send a response back via event.source
event.source.postMessage(
{ status: 'ready', widgetVersion: '2.1.0' },
event.origin // Send back to the origin that messaged us
);
}
});
The targetOrigin Parameter
The targetOrigin parameter is a security feature. The browser only delivers the message if the target window's current origin matches targetOrigin. This prevents your message from being delivered to a different page if the target window navigated away.
// ✅ Secure: message only delivered if popup is on the expected origin
popup.postMessage(sensitiveData, 'https://trusted-site.com');
// ❌ Insecure: message delivered regardless of the popup's current origin
popup.postMessage(sensitiveData, '*');
// If the popup navigated to a malicious site, that site receives your data!
When to use '*':
// Acceptable: non-sensitive data where origin doesn't matter
popup.postMessage({ type: 'ping' }, '*');
// Acceptable: when you genuinely need to send to any origin
// (e.g., a debugging tool that works with any page)
iframe.contentWindow.postMessage({ action: 'resize' }, '*');
Never use '*' as targetOrigin when sending sensitive data like authentication tokens, user information, or any data that could be exploited if received by an unintended party. Always specify the exact expected origin.
Sending Complex Data
postMessage uses the structured clone algorithm, which supports a wide range of data types:
// All of these work
popup.postMessage('Simple string', targetOrigin);
popup.postMessage(42, targetOrigin);
popup.postMessage(true, targetOrigin);
popup.postMessage(null, targetOrigin);
popup.postMessage([1, 2, 3], targetOrigin);
popup.postMessage({ key: 'value', nested: { a: 1 } }, targetOrigin);
popup.postMessage(new Date(), targetOrigin);
popup.postMessage(new Map([['key', 'value']]), targetOrigin);
popup.postMessage(new Set([1, 2, 3]), targetOrigin);
popup.postMessage(new Uint8Array([1, 2, 3]), targetOrigin);
// These do NOT work (throw DataCloneError)
popup.postMessage(function() {}, targetOrigin); // Functions
popup.postMessage(document.body, targetOrigin); // DOM nodes
popup.postMessage(Symbol('x'), targetOrigin); // Symbols
Transferable Objects
For large binary data like ArrayBuffer, you can transfer ownership instead of copying. This is much faster for large data but makes the original buffer unusable:
const buffer = new ArrayBuffer(1024 * 1024); // 1MB buffer
// Transfer (fast, but buffer becomes unusable in sender)
popup.postMessage(buffer, targetOrigin, [buffer]);
console.log(buffer.byteLength); // 0 (buffer has been transferred)
// Copy (slower for large data, but original remains usable)
popup.postMessage(buffer, targetOrigin);
console.log(buffer.byteLength); // 1048576 (still usable)
The message Event and Security Checks
The message event is where you receive and process data from other windows. Proper security validation in the event handler is critical, because any window that has a reference to yours can send messages to it.
The MessageEvent Object
The event object provides everything you need to validate and respond to messages:
window.addEventListener('message', (event) => {
// The data that was sent
console.log(event.data);
// The origin of the sender (e.g., "https://other-site.com")
console.log(event.origin);
// A reference to the sender's window object
console.log(event.source);
// The ports transferred with the message (for MessageChannel)
console.log(event.ports);
});
Always Validate the Origin
The most important security practice is checking event.origin before processing any message:
// ❌ DANGEROUS: processes messages from any origin
window.addEventListener('message', (event) => {
processData(event.data); // A malicious site could send fake data
});
// ✅ SECURE: only processes messages from trusted origins
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-site.com') {
return; // Ignore messages from unknown origins
}
processData(event.data);
});
For applications that communicate with multiple trusted origins:
const TRUSTED_ORIGINS = new Set([
'https://app.mysite.com',
'https://widget.mysite.com',
'https://auth.trusted-partner.com'
]);
window.addEventListener('message', (event) => {
if (!TRUSTED_ORIGINS.has(event.origin)) {
console.warn('Message from untrusted origin:', event.origin);
return;
}
handleMessage(event);
});
Validate the Message Structure
Beyond checking the origin, validate the message data structure. Even trusted origins might send malformed data due to bugs or version mismatches:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-site.com') return;
const { data } = event;
// Validate message structure
if (!data || typeof data !== 'object') {
console.warn('Invalid message format');
return;
}
if (typeof data.type !== 'string') {
console.warn('Message missing type field');
return;
}
// Route messages by type
switch (data.type) {
case 'auth-result':
if (typeof data.token === 'string') {
handleAuthResult(data.token);
}
break;
case 'resize':
if (typeof data.height === 'number' && data.height > 0) {
handleResize(data.height);
}
break;
default:
console.warn('Unknown message type:', data.type);
}
});
Never Use eval or innerHTML with Message Data
Message data should never be treated as code or unescaped HTML:
window.addEventListener('message', (event) => {
if (event.origin !== 'https://trusted-site.com') return;
// ❌ DANGEROUS: executing received data as code
eval(event.data.code);
// ❌ DANGEROUS: inserting received data as HTML (XSS risk)
document.body.innerHTML = event.data.html;
// ✅ SAFE: use data as values, not as code or markup
document.getElementById('output').textContent = event.data.text;
});
Building a Robust Message Protocol
For applications with complex cross-window communication, define a structured protocol:
// Shared message protocol (both sides agree on this format)
// {
// type: string, (message type identifier)
// id: string, (unique message ID for request/response matching)
// payload: any, (the actual data)
// error: string|null (error message if applicable)
// }
class CrossWindowMessenger {
constructor(targetWindow, targetOrigin) {
this.target = targetWindow;
this.targetOrigin = targetOrigin;
this.pendingRequests = new Map();
this.handlers = new Map();
window.addEventListener('message', (event) => {
this.handleMessage(event);
});
}
// Register a handler for a message type
on(type, handler) {
this.handlers.set(type, handler);
}
// Send a message and wait for a response
request(type, payload, timeout = 5000) {
return new Promise((resolve, reject) => {
const id = crypto.randomUUID();
const timer = setTimeout(() => {
this.pendingRequests.delete(id);
reject(new Error(`Request "${type}" timed out after ${timeout}ms`));
}, timeout);
this.pendingRequests.set(id, { resolve, reject, timer });
this.target.postMessage(
{ type, id, payload, error: null },
this.targetOrigin
);
});
}
// Send a message without expecting a response
send(type, payload) {
this.target.postMessage(
{ type, id: null, payload, error: null },
this.targetOrigin
);
}
handleMessage(event) {
if (event.origin !== this.targetOrigin) return;
const { type, id, payload, error } = event.data;
// Check if this is a response to a pending request
if (id && this.pendingRequests.has(id)) {
const { resolve, reject, timer } = this.pendingRequests.get(id);
clearTimeout(timer);
this.pendingRequests.delete(id);
if (error) {
reject(new Error(error));
} else {
resolve(payload);
}
return;
}
// Handle incoming request/message
const handler = this.handlers.get(type);
if (handler) {
try {
const result = handler(payload);
// If the message has an ID, send a response
if (id && event.source) {
Promise.resolve(result).then(response => {
event.source.postMessage(
{ type: `${type}:response`, id, payload: response, error: null },
event.origin
);
}).catch(err => {
event.source.postMessage(
{ type: `${type}:response`, id, payload: null, error: err.message },
event.origin
);
});
}
} catch (err) {
if (id && event.source) {
event.source.postMessage(
{ type: `${type}:response`, id, payload: null, error: err.message },
event.origin
);
}
}
}
}
destroy() {
for (const { timer } of this.pendingRequests.values()) {
clearTimeout(timer);
}
this.pendingRequests.clear();
this.handlers.clear();
}
}
Usage from the parent page:
const popup = window.open('https://widget.example.com', 'widget', 'width=500,height=400');
const messenger = new CrossWindowMessenger(popup, 'https://widget.example.com');
// Request-response pattern
async function getUserData() {
try {
const userData = await messenger.request('getUserData', { userId: 123 });
console.log('User data:', userData);
} catch (error) {
console.error('Failed:', error.message);
}
}
// Fire-and-forget pattern
messenger.send('updateTheme', { dark: true });
Usage from the popup:
const messenger = new CrossWindowMessenger(window.opener, 'https://mysite.com');
// Handle requests
messenger.on('getUserData', async (payload) => {
const user = await fetchUser(payload.userId);
return user; // Sent back as the response
});
// Handle notifications
messenger.on('updateTheme', (payload) => {
document.body.classList.toggle('dark', payload.dark);
});
Iframes: contentWindow and contentDocument
Iframes are the most common way to embed one page inside another. They create a separate browsing context with its own window, document, and execution environment. Communication between the parent page and the iframe follows the same-origin rules.
Accessing Iframe Windows
From the parent page, you access an iframe's window through the contentWindow property:
<iframe id="my-iframe" src="https://mysite.com/embedded.html"></iframe>
<script>
const iframe = document.getElementById('my-iframe');
// Access the iframe's window object
const iframeWindow = iframe.contentWindow;
// Wait for the iframe to load before interacting
iframe.addEventListener('load', () => {
console.log('Iframe loaded');
// Same-origin: full access
if (iframe.src.startsWith(window.location.origin)) {
console.log(iframeWindow.document.title);
iframeWindow.someFunction();
}
});
</script>
contentWindow vs. contentDocument
| Property | Returns | Equivalent |
|---|---|---|
iframe.contentWindow | The iframe's window object | Direct access |
iframe.contentDocument | The iframe's document object | iframe.contentWindow.document |
const iframe = document.getElementById('my-iframe');
// These are equivalent (same-origin only)
const doc1 = iframe.contentDocument;
const doc2 = iframe.contentWindow.document;
console.log(doc1 === doc2); // true
contentDocument returns null for cross-origin iframes. Always use contentWindow.postMessage() for cross-origin iframe communication.
Accessing the Parent from Inside an Iframe
From within an iframe, you can access the parent page through several properties:
// Inside the iframe:
// Reference to the parent window
window.parent
// Reference to the topmost window (useful for nested iframes)
window.top
// Check if we're inside an iframe
if (window !== window.top) {
console.log('We are inside an iframe');
}
// Check if we're inside an iframe (alternative)
if (window.parent !== window) {
console.log('We are inside an iframe');
}
// Same-origin parent: full access
window.parent.document.title = 'Updated from iframe';
window.parent.someFunction();
// Cross-origin parent: only postMessage
window.parent.postMessage({ type: 'ready' }, 'https://parent-site.com');
The window.frames Collection
A page can access its iframes through the window.frames collection:
<iframe name="header" src="header.html"></iframe>
<iframe name="content" src="content.html"></iframe>
<iframe name="footer" src="footer.html"></iframe>
<script>
// Access by index
console.log(window.frames[0]); // First iframe's window
console.log(window.frames.length); // 3
// Access by name
console.log(window.frames.header); // The "header" iframe's window
console.log(window.frames.content); // The "content" iframe's window
</script>
Same-Origin Iframe Communication
When the parent and iframe share the same origin, they can directly call functions and access each other's DOM:
Parent page:
<iframe id="editor-frame" src="/editor.html"></iframe>
<script>
const editorFrame = document.getElementById('editor-frame');
editorFrame.addEventListener('load', () => {
const editorWindow = editorFrame.contentWindow;
// Call a function defined in the iframe
editorWindow.loadContent('Hello, World!');
// Read data from the iframe
const content = editorWindow.getContent();
console.log('Editor content:', content);
});
// Function the iframe can call
function onEditorSave(content) {
console.log('Saving:', content);
fetch('/api/save', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ content })
});
}
</script>
Iframe page (/editor.html):
<div id="editor" contenteditable="true"></div>
<script>
const editor = document.getElementById('editor');
// Functions callable from the parent
function loadContent(html) {
editor.innerHTML = html;
}
function getContent() {
return editor.innerHTML;
}
// Call a function in the parent
document.getElementById('save-btn').addEventListener('click', () => {
window.parent.onEditorSave(getContent());
});
</script>
Cross-Origin Iframe Communication
For cross-origin iframes, postMessage is the only way to communicate:
Parent page:
<iframe id="widget" src="https://widget-provider.com/embed"></iframe>
<script>
const widget = document.getElementById('widget');
// Send configuration to the widget after it loads
widget.addEventListener('load', () => {
widget.contentWindow.postMessage(
{ type: 'configure', theme: 'dark', language: 'en' },
'https://widget-provider.com'
);
});
// Listen for events from the widget
window.addEventListener('message', (event) => {
if (event.origin !== 'https://widget-provider.com') return;
switch (event.data.type) {
case 'widget-ready':
console.log('Widget is ready');
break;
case 'widget-resize':
// Auto-resize the iframe to fit its content
widget.style.height = event.data.height + 'px';
break;
case 'widget-action':
console.log('User action in widget:', event.data.action);
break;
}
});
</script>
Widget page (https://widget-provider.com/embed):
<script>
// Notify the parent that we're ready
window.parent.postMessage(
{ type: 'widget-ready' },
'*' // We don't know the parent's origin ahead of time
);
// Request resize when content changes
function notifyResize() {
const height = document.documentElement.scrollHeight;
window.parent.postMessage(
{ type: 'widget-resize', height },
'*'
);
}
// Watch for content changes
const observer = new MutationObserver(notifyResize);
observer.observe(document.body, { childList: true, subtree: true });
// Also resize on window resize
window.addEventListener('resize', notifyResize);
// Listen for configuration from the parent
window.addEventListener('message', (event) => {
// In a real widget, you might validate event.origin
// against a list of known customer domains
if (event.data.type === 'configure') {
applyTheme(event.data.theme);
setLanguage(event.data.language);
}
});
</script>
Auto-Resizing Iframes
A common pattern is making an iframe automatically resize to fit its content, eliminating scrollbars:
// Parent page
function setupAutoResize(iframe, trustedOrigin) {
window.addEventListener('message', (event) => {
if (trustedOrigin && event.origin !== trustedOrigin) return;
if (event.source !== iframe.contentWindow) return;
if (event.data.type === 'resize' && typeof event.data.height === 'number') {
iframe.style.height = Math.max(event.data.height, 100) + 'px';
}
});
}
const iframe = document.getElementById('my-iframe');
setupAutoResize(iframe, 'https://widget-site.com');
// Inside the iframe
function reportHeight() {
const height = document.documentElement.scrollHeight;
window.parent.postMessage({ type: 'resize', height }, '*');
}
// Report on load
reportHeight();
// Report on content changes
const observer = new ResizeObserver(reportHeight);
observer.observe(document.body);
// Report on image loads (which change layout)
document.addEventListener('load', reportHeight, true);
Iframe Sandboxing
The sandbox attribute restricts what an iframe can do, providing an additional security layer beyond the same-origin policy:
<!-- Fully sandboxed: almost everything is disabled -->
<iframe src="untrusted-content.html" sandbox></iframe>
<!-- Selectively allow features -->
<iframe src="widget.html" sandbox="allow-scripts allow-forms"></iframe>
<!-- Common sandbox for third-party widgets -->
<iframe
src="https://widget.example.com"
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
></iframe>
Available sandbox flags:
| Flag | What it allows |
|---|---|
allow-scripts | JavaScript execution |
allow-same-origin | Treated as same-origin (for cookies, storage) |
allow-forms | Form submission |
allow-popups | window.open() and target="_blank" |
allow-popups-to-escape-sandbox | Popups without sandbox restrictions |
allow-top-navigation | Navigate the parent page |
allow-modals | alert(), confirm(), prompt() |
allow-downloads | File downloads |
Be careful with allow-scripts allow-same-origin together. If the iframe content is from the same origin, this combination allows the iframe to remove its own sandbox by accessing the parent and modifying the <iframe> element. Only use this combination when the iframe content is from a different origin.
Practical Example: Embedded Payment Form
A real-world pattern where an iframe isolates sensitive payment data:
Merchant page:
<div id="payment-container">
<h3>Payment Details</h3>
<iframe
id="payment-iframe"
src="https://payments.provider.com/form"
sandbox="allow-scripts allow-forms"
style="border: none; width: 100%; height: 250px;"
></iframe>
<button id="pay-btn" disabled>Pay Now</button>
</div>
<script>
const paymentIframe = document.getElementById('payment-iframe');
const payButton = document.getElementById('pay-btn');
window.addEventListener('message', (event) => {
if (event.origin !== 'https://payments.provider.com') return;
switch (event.data.type) {
case 'form-ready':
payButton.disabled = false;
break;
case 'form-valid':
payButton.disabled = !event.data.isValid;
break;
case 'payment-success':
showSuccess(event.data.transactionId);
break;
case 'payment-error':
showError(event.data.message);
break;
}
});
payButton.addEventListener('click', () => {
payButton.disabled = true;
payButton.textContent = 'Processing...';
paymentIframe.contentWindow.postMessage(
{ type: 'submit-payment', orderId: 'ORD-12345', amount: 29.99 },
'https://payments.provider.com'
);
});
</script>
The payment form runs in a sandboxed iframe from the payment provider's domain. The merchant page never sees the credit card number. Communication happens exclusively through postMessage with strict origin checks.
Cross-Tab Communication via BroadcastChannel
While not directly related to iframes or popups, the BroadcastChannel API provides a simpler way to communicate between same-origin tabs, windows, and iframes without needing window references:
// In any tab/iframe on the same origin
const channel = new BroadcastChannel('app-events');
// Send a message to all other contexts on the same origin
channel.postMessage({ type: 'user-logged-in', userId: 123 });
// Receive messages from other contexts
channel.onmessage = (event) => {
console.log('Received:', event.data);
if (event.data.type === 'user-logged-in') {
updateUIForLoggedInUser(event.data.userId);
}
};
// Close when done
channel.close();
BroadcastChannel is simpler than postMessage for same-origin scenarios because you do not need a reference to the target window. All contexts that create a channel with the same name receive each other's messages automatically.
// Tab 1: Update theme
const channel = new BroadcastChannel('theme');
document.getElementById('dark-mode-toggle').addEventListener('change', (e) => {
const isDark = e.target.checked;
channel.postMessage({ theme: isDark ? 'dark' : 'light' });
applyTheme(isDark ? 'dark' : 'light');
});
// Tab 2 (and 3, 4, etc.): Receive theme changes
const channel = new BroadcastChannel('theme');
channel.onmessage = (event) => {
applyTheme(event.data.theme);
};
BroadcastChannel only works between same-origin contexts. For cross-origin communication, you must use window.postMessage(). Also, BroadcastChannel messages are not delivered to the sender, only to other contexts with the same channel name.
Summary
Cross-window communication in JavaScript is governed by the Same-Origin Policy and enabled by dedicated APIs:
- The Same-Origin Policy allows full mutual access between windows that share the same protocol, hostname, and port. Cross-origin windows are almost completely isolated, with only a few properties like
closed,close(),focus(), andpostMessage()remaining accessible. window.postMessage(data, targetOrigin)is the standard API for cross-origin communication. Always specify the exacttargetOriginwhen sending sensitive data. Never use'*'unless the data is non-sensitive.- The
messageevent delivers received messages withevent.data,event.origin, andevent.source. Always validateevent.originagainst a trusted list before processing any message. Validate the message structure and never use message data ineval()orinnerHTML. - Iframes provide
contentWindow(the iframe'swindow) andcontentDocument(the iframe'sdocument, same-origin only). From inside an iframe,window.parentandwindow.topreference the parent and topmost windows. Use thesandboxattribute to restrict iframe capabilities. - For same-origin cross-tab communication without window references,
BroadcastChannelprovides a simpler alternative topostMessage. - Build structured message protocols with type identifiers, request IDs, and timeout handling for robust cross-window applications.