JavaScript Channel Messaging API: Complete Port Communication Guide
Master the Channel Messaging API for creating direct communication channels. Learn MessagePort, MessageChannel, and secure cross-origin messaging.
JavaScript Channel Messaging API: Complete Port Communication Guide
The Channel Messaging API enables direct, two-way communication channels between different browsing contexts through MessagePort objects, providing secure and efficient message passing.
Understanding the Channel Messaging API
The Channel Messaging API creates pairs of connected ports that can be used to communicate between different contexts like windows, iframes, web workers, and service workers.
// Create a new message channel
const channel = new MessageChannel();
// Access the two ports
const port1 = channel.port1;
const port2 = channel.port2;
// Set up message handler on port1
port1.onmessage = (event) => {
console.log('Port1 received:', event.data);
};
// Send message from port2
port2.postMessage('Hello from port2!');
// Ports must be started if using addEventListener
port1.start();
port2.start();
// Transfer port to another context
// otherWindow.postMessage('init', '*', [port2]);
// Close ports when done
port1.close();
port2.close();
Basic Port Communication
Port Manager
class PortManager {
constructor() {
this.ports = new Map();
this.channels = new Map();
this.handlers = new Map();
this.messageQueue = new Map();
this.debug = false;
}
// Create a new channel
createChannel(channelId) {
if (this.channels.has(channelId)) {
throw new Error(`Channel ${channelId} already exists`);
}
const channel = new MessageChannel();
this.channels.set(channelId, channel);
this.ports.set(`${channelId}-port1`, channel.port1);
this.ports.set(`${channelId}-port2`, channel.port2);
// Setup default handlers
this.setupPort(channel.port1, `${channelId}-port1`);
this.setupPort(channel.port2, `${channelId}-port2`);
if (this.debug) {
console.log(`Created channel: ${channelId}`);
}
return channel;
}
// Setup port with error handling and logging
setupPort(port, portId) {
port.onmessage = (event) => {
this.handleMessage(portId, event);
};
port.onmessageerror = (event) => {
this.handleError(portId, event);
};
// Start the port
port.start();
}
// Get a specific port
getPort(portId) {
return this.ports.get(portId);
}
// Send message through port
sendMessage(portId, message, transfer = []) {
const port = this.ports.get(portId);
if (!port) {
throw new Error(`Port ${portId} not found`);
}
try {
const envelope = {
id: this.generateMessageId(),
timestamp: Date.now(),
data: message,
};
port.postMessage(envelope, transfer);
if (this.debug) {
console.log(`Sent message on ${portId}:`, envelope);
}
return envelope.id;
} catch (error) {
console.error(`Failed to send message on ${portId}:`, error);
throw error;
}
}
// Handle incoming messages
handleMessage(portId, event) {
const message = event.data;
if (this.debug) {
console.log(`Received message on ${portId}:`, message);
}
// Check for queued messages
if (this.messageQueue.has(portId)) {
const queue = this.messageQueue.get(portId);
queue.push(message);
}
// Execute handlers
const handlers = this.handlers.get(portId) || [];
handlers.forEach((handler) => {
try {
handler(message.data, message, event);
} catch (error) {
console.error(`Handler error on ${portId}:`, error);
}
});
}
// Handle message errors
handleError(portId, event) {
console.error(`Message error on ${portId}:`, event);
}
// Register message handler
onMessage(portId, handler) {
if (!this.handlers.has(portId)) {
this.handlers.set(portId, []);
}
this.handlers.get(portId).push(handler);
// Return unsubscribe function
return () => {
const handlers = this.handlers.get(portId);
const index = handlers.indexOf(handler);
if (index > -1) {
handlers.splice(index, 1);
}
};
}
// Request-response pattern
request(portId, message, timeout = 5000) {
return new Promise((resolve, reject) => {
const messageId = this.sendMessage(portId, {
...message,
expectResponse: true,
});
const cleanup = this.onMessage(portId, (data, envelope) => {
if (envelope.id === messageId || data.responseToId === messageId) {
cleanup();
clearTimeout(timeoutId);
resolve(data);
}
});
const timeoutId = setTimeout(() => {
cleanup();
reject(new Error(`Request timeout on ${portId}`));
}, timeout);
});
}
// Transfer port to another context
transferPort(portId, targetWindow, targetOrigin = '*', message = {}) {
const port = this.ports.get(portId);
if (!port) {
throw new Error(`Port ${portId} not found`);
}
// Remove from local management
this.ports.delete(portId);
// Transfer port
targetWindow.postMessage(
{
type: 'port-transfer',
portId,
...message,
},
targetOrigin,
[port]
);
if (this.debug) {
console.log(`Transferred port ${portId} to ${targetOrigin}`);
}
}
// Receive transferred port
receivePort(portId, port) {
this.ports.set(portId, port);
this.setupPort(port, portId);
if (this.debug) {
console.log(`Received port: ${portId}`);
}
}
// Close port
closePort(portId) {
const port = this.ports.get(portId);
if (port) {
port.close();
this.ports.delete(portId);
this.handlers.delete(portId);
if (this.debug) {
console.log(`Closed port: ${portId}`);
}
}
}
// Close channel
closeChannel(channelId) {
this.closePort(`${channelId}-port1`);
this.closePort(`${channelId}-port2`);
this.channels.delete(channelId);
}
// Generate unique message ID
generateMessageId() {
return `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
// Enable message queuing
enableQueue(portId) {
if (!this.messageQueue.has(portId)) {
this.messageQueue.set(portId, []);
}
}
// Get queued messages
getQueuedMessages(portId) {
const queue = this.messageQueue.get(portId) || [];
this.messageQueue.delete(portId);
return queue;
}
}
// Usage
const portManager = new PortManager();
portManager.debug = true;
// Create a channel
const channel = portManager.createChannel('main');
// Listen on port1
portManager.onMessage('main-port1', (data) => {
console.log('Port1 received:', data);
});
// Send from port2
portManager.sendMessage('main-port2', {
type: 'greeting',
text: 'Hello from port2!',
});
// Request-response
portManager
.request('main-port2', {
type: 'get-data',
key: 'user',
})
.then((response) => {
console.log('Response:', response);
});
Worker Communication
Enhanced Worker Communication
class WorkerPortCommunicator {
constructor() {
this.workers = new Map();
this.portManager = new PortManager();
}
// Create worker with port
createWorker(workerId, scriptUrl, options = {}) {
const channel = this.portManager.createChannel(`worker-${workerId}`);
const worker = new Worker(scriptUrl, options);
// Store worker reference
this.workers.set(workerId, {
worker,
port: channel.port1,
channel,
});
// Transfer port2 to worker
worker.postMessage(
{
type: 'init',
port: channel.port2,
},
[channel.port2]
);
// Setup main thread port
this.setupMainThreadPort(workerId);
return worker;
}
// Setup main thread port handlers
setupMainThreadPort(workerId) {
const portId = `worker-${workerId}-port1`;
// Handle worker messages
this.portManager.onMessage(portId, (data, envelope) => {
this.handleWorkerMessage(workerId, data, envelope);
});
// Handle worker errors
const worker = this.workers.get(workerId).worker;
worker.onerror = (error) => {
console.error(`Worker ${workerId} error:`, error);
this.handleWorkerError(workerId, error);
};
}
// Send message to worker
sendToWorker(workerId, message, transfer = []) {
const portId = `worker-${workerId}-port1`;
return this.portManager.sendMessage(portId, message, transfer);
}
// Request from worker
requestFromWorker(workerId, message, timeout = 5000) {
const portId = `worker-${workerId}-port1`;
return this.portManager.request(portId, message, timeout);
}
// Handle worker messages
handleWorkerMessage(workerId, data, envelope) {
// Emit custom event
const event = new CustomEvent('worker-message', {
detail: {
workerId,
data,
envelope,
},
});
window.dispatchEvent(event);
}
// Handle worker errors
handleWorkerError(workerId, error) {
const event = new CustomEvent('worker-error', {
detail: {
workerId,
error,
},
});
window.dispatchEvent(event);
}
// Terminate worker
terminateWorker(workerId) {
const workerInfo = this.workers.get(workerId);
if (workerInfo) {
// Close port
this.portManager.closeChannel(`worker-${workerId}`);
// Terminate worker
workerInfo.worker.terminate();
// Clean up
this.workers.delete(workerId);
}
}
// Broadcast to all workers
broadcastToWorkers(message, transfer = []) {
const promises = [];
this.workers.forEach((workerInfo, workerId) => {
promises.push(this.sendToWorker(workerId, message, transfer));
});
return Promise.all(promises);
}
// Create worker pool
createWorkerPool(poolSize, scriptUrl, options = {}) {
const pool = {
workers: [],
currentIndex: 0,
};
for (let i = 0; i < poolSize; i++) {
const workerId = `pool-worker-${i}`;
const worker = this.createWorker(workerId, scriptUrl, options);
pool.workers.push(workerId);
}
return {
execute: (message) => {
const workerId = pool.workers[pool.currentIndex];
pool.currentIndex = (pool.currentIndex + 1) % poolSize;
return this.requestFromWorker(workerId, message);
},
broadcast: (message) => {
return Promise.all(
pool.workers.map((workerId) => this.sendToWorker(workerId, message))
);
},
terminate: () => {
pool.workers.forEach((workerId) => this.terminateWorker(workerId));
},
};
}
}
// Worker script (worker.js)
const workerScript = `
let port;
self.onmessage = (event) => {
if (event.data.type === 'init' && event.data.port) {
port = event.data.port;
setupPort();
}
};
function setupPort() {
port.onmessage = async (event) => {
const message = event.data;
console.log('Worker received:', message);
// Handle different message types
switch (message.data.type) {
case 'compute':
const result = await performComputation(message.data.input);
port.postMessage({
id: message.id,
responseToId: message.id,
data: { result }
});
break;
case 'get-status':
port.postMessage({
id: message.id,
responseToId: message.id,
data: {
status: 'ready',
memory: performance.memory
}
});
break;
}
};
// Notify ready
port.postMessage({
data: { type: 'ready' }
});
}
async function performComputation(input) {
// Simulate heavy computation
let result = 0;
for (let i = 0; i < input; i++) {
result += Math.sqrt(i);
}
return result;
}
`;
// Create blob URL for worker
const blob = new Blob([workerScript], { type: 'application/javascript' });
const workerUrl = URL.createObjectURL(blob);
// Usage
const communicator = new WorkerPortCommunicator();
// Create single worker
const worker = communicator.createWorker('compute-1', workerUrl);
// Listen for worker messages
window.addEventListener('worker-message', (event) => {
console.log('Worker message:', event.detail);
});
// Send computation request
communicator
.requestFromWorker('compute-1', {
type: 'compute',
input: 1000000,
})
.then((result) => {
console.log('Computation result:', result);
});
// Create worker pool
const pool = communicator.createWorkerPool(4, workerUrl);
// Execute tasks on pool
Promise.all([
pool.execute({ type: 'compute', input: 100000 }),
pool.execute({ type: 'compute', input: 200000 }),
pool.execute({ type: 'compute', input: 300000 }),
]).then((results) => {
console.log('Pool results:', results);
});
Cross-Frame Communication
Secure Frame Messaging
class FrameMessenger {
constructor(options = {}) {
this.options = {
targetOrigin: '*',
timeout: 5000,
handshakeTimeout: 10000,
...options,
};
this.frames = new Map();
this.portManager = new PortManager();
this.pendingHandshakes = new Map();
this.init();
}
// Initialize frame messenger
init() {
// Listen for incoming connections
window.addEventListener('message', (event) => {
this.handleMessage(event);
});
}
// Handle incoming messages
handleMessage(event) {
// Validate origin if specified
if (
this.options.targetOrigin !== '*' &&
event.origin !== this.options.targetOrigin
) {
console.warn('Rejected message from origin:', event.origin);
return;
}
const { type, frameId, data } = event.data || {};
switch (type) {
case 'frame-handshake-request':
this.handleHandshakeRequest(event);
break;
case 'frame-handshake-response':
this.handleHandshakeResponse(event);
break;
case 'frame-port-transfer':
this.handlePortTransfer(event);
break;
}
}
// Connect to frame
connectToFrame(frame, frameId, options = {}) {
return new Promise((resolve, reject) => {
const channel = this.portManager.createChannel(`frame-${frameId}`);
// Store pending handshake
this.pendingHandshakes.set(frameId, {
resolve,
reject,
channel,
timeout: setTimeout(() => {
this.pendingHandshakes.delete(frameId);
reject(new Error('Handshake timeout'));
}, options.handshakeTimeout || this.options.handshakeTimeout),
});
// Send handshake request
frame.postMessage(
{
type: 'frame-handshake-request',
frameId,
origin: window.location.origin,
},
options.targetOrigin || this.options.targetOrigin
);
});
}
// Handle handshake request
handleHandshakeRequest(event) {
const { frameId, origin } = event.data;
// Create channel for this frame
const channel = this.portManager.createChannel(`frame-${frameId}`);
// Store frame info
this.frames.set(frameId, {
frameId,
origin,
source: event.source,
port: channel.port1,
connectedAt: Date.now(),
});
// Setup port handlers
this.setupFramePort(frameId);
// Send handshake response with port
event.source.postMessage(
{
type: 'frame-handshake-response',
frameId,
success: true,
},
event.origin
);
// Transfer port
setTimeout(() => {
event.source.postMessage(
{
type: 'frame-port-transfer',
frameId,
port: channel.port2,
},
event.origin,
[channel.port2]
);
}, 100);
}
// Handle handshake response
handleHandshakeResponse(event) {
const { frameId, success } = event.data;
const pending = this.pendingHandshakes.get(frameId);
if (pending && success) {
clearTimeout(pending.timeout);
// Store frame info
this.frames.set(frameId, {
frameId,
origin: event.origin,
source: event.source,
port: pending.channel.port1,
connectedAt: Date.now(),
});
// Setup port handlers
this.setupFramePort(frameId);
pending.resolve({
frameId,
port: pending.channel.port1,
});
this.pendingHandshakes.delete(frameId);
}
}
// Handle port transfer
handlePortTransfer(event) {
const { frameId, port } = event.data;
if (event.ports && event.ports[0]) {
const pending = this.pendingHandshakes.get(frameId);
if (pending) {
// Complete handshake
this.portManager.receivePort(`frame-${frameId}-port2`, event.ports[0]);
// Update frame info
const frame = this.frames.get(frameId);
if (frame) {
frame.port = event.ports[0];
}
} else {
// Direct port transfer
this.portManager.receivePort(`frame-${frameId}-port`, event.ports[0]);
this.setupFramePort(frameId);
}
}
}
// Setup frame port handlers
setupFramePort(frameId) {
const portId = `frame-${frameId}-port1`;
// Handle frame messages
this.portManager.onMessage(portId, (data, envelope) => {
this.handleFrameMessage(frameId, data, envelope);
});
// Emit connected event
const event = new CustomEvent('frame-connected', {
detail: { frameId },
});
window.dispatchEvent(event);
}
// Handle frame messages
handleFrameMessage(frameId, data, envelope) {
const event = new CustomEvent('frame-message', {
detail: {
frameId,
data,
envelope,
},
});
window.dispatchEvent(event);
}
// Send to frame
sendToFrame(frameId, message, transfer = []) {
const frame = this.frames.get(frameId);
if (!frame) {
throw new Error(`Frame ${frameId} not connected`);
}
const portId = `frame-${frameId}-port1`;
return this.portManager.sendMessage(portId, message, transfer);
}
// Request from frame
requestFromFrame(frameId, message, timeout) {
const portId = `frame-${frameId}-port1`;
return this.portManager.request(
portId,
message,
timeout || this.options.timeout
);
}
// Broadcast to all frames
broadcastToFrames(message, transfer = []) {
const promises = [];
this.frames.forEach((frame, frameId) => {
promises.push(this.sendToFrame(frameId, message, transfer));
});
return Promise.all(promises);
}
// Disconnect frame
disconnectFrame(frameId) {
const frame = this.frames.get(frameId);
if (frame) {
// Close port
this.portManager.closeChannel(`frame-${frameId}`);
// Remove frame
this.frames.delete(frameId);
// Emit disconnected event
const event = new CustomEvent('frame-disconnected', {
detail: { frameId },
});
window.dispatchEvent(event);
}
}
// Get connected frames
getConnectedFrames() {
return Array.from(this.frames.values()).map((frame) => ({
frameId: frame.frameId,
origin: frame.origin,
connectedAt: frame.connectedAt,
}));
}
}
// Parent window usage
const messenger = new FrameMessenger({
targetOrigin: 'https://trusted-domain.com',
});
// Connect to iframe
const iframe = document.getElementById('my-iframe');
messenger
.connectToFrame(iframe.contentWindow, 'iframe-1')
.then(({ frameId, port }) => {
console.log('Connected to frame:', frameId);
// Send message
messenger.sendToFrame(frameId, {
type: 'config',
theme: 'dark',
});
});
// Listen for frame messages
window.addEventListener('frame-message', (event) => {
console.log('Frame message:', event.detail);
});
// Child frame usage
const childMessenger = new FrameMessenger();
// Wait for connection
window.addEventListener('frame-connected', (event) => {
const frameId = event.detail.frameId;
// Send message to parent
childMessenger.sendToFrame(frameId, {
type: 'ready',
capabilities: ['feature1', 'feature2'],
});
});
Shared State Management
Distributed State with Ports
class PortBasedStateManager {
constructor(options = {}) {
this.options = {
syncInterval: 1000,
conflictResolution: 'vector-clock',
...options,
};
this.state = {};
this.version = 0;
this.vectorClock = new Map();
this.peers = new Map();
this.portManager = new PortManager();
this.nodeId = this.generateNodeId();
this.init();
}
// Initialize state manager
init() {
// Initialize vector clock
this.vectorClock.set(this.nodeId, 0);
// Start sync timer
if (this.options.syncInterval > 0) {
this.startSyncTimer();
}
}
// Connect to peer
connectToPeer(peerId, port) {
// Receive the port
this.portManager.receivePort(`peer-${peerId}`, port);
// Store peer info
this.peers.set(peerId, {
peerId,
port,
lastSync: 0,
vectorClock: new Map(),
});
// Setup handlers
this.setupPeerHandlers(peerId);
// Initial sync
this.syncWithPeer(peerId);
}
// Setup peer handlers
setupPeerHandlers(peerId) {
const portId = `peer-${peerId}`;
this.portManager.onMessage(portId, (data) => {
this.handlePeerMessage(peerId, data);
});
}
// Handle peer messages
handlePeerMessage(peerId, message) {
switch (message.type) {
case 'state-update':
this.handleStateUpdate(peerId, message.data);
break;
case 'sync-request':
this.handleSyncRequest(peerId, message.data);
break;
case 'sync-response':
this.handleSyncResponse(peerId, message.data);
break;
case 'vector-clock-update':
this.handleVectorClockUpdate(peerId, message.data);
break;
}
}
// Update state
updateState(path, value) {
// Update local state
this.setValueAtPath(this.state, path, value);
// Increment vector clock
this.vectorClock.set(this.nodeId, this.vectorClock.get(this.nodeId) + 1);
this.version++;
// Create update message
const update = {
path,
value,
version: this.version,
vectorClock: this.serializeVectorClock(),
timestamp: Date.now(),
nodeId: this.nodeId,
};
// Broadcast to peers
this.broadcastToPeers('state-update', update);
return update;
}
// Handle state update from peer
handleStateUpdate(peerId, update) {
const { path, value, vectorClock, nodeId } = update;
// Update peer's vector clock
const peer = this.peers.get(peerId);
if (peer) {
peer.vectorClock = this.deserializeVectorClock(vectorClock);
}
// Check if update should be applied
if (this.shouldApplyUpdate(update)) {
// Apply update
this.setValueAtPath(this.state, path, value);
// Update vector clock
this.mergeVectorClock(vectorClock);
// Emit change event
this.emitChange(path, value, update);
} else {
// Handle conflict
this.handleConflict(path, value, update);
}
}
// Check if update should be applied
shouldApplyUpdate(update) {
const { vectorClock, nodeId } = update;
const remoteVC = this.deserializeVectorClock(vectorClock);
// Compare vector clocks
return (
this.isVectorClockConcurrent(remoteVC) ||
this.isVectorClockGreater(remoteVC)
);
}
// Compare vector clocks
isVectorClockGreater(remoteVC) {
let hasGreater = false;
let hasLess = false;
remoteVC.forEach((value, nodeId) => {
const localValue = this.vectorClock.get(nodeId) || 0;
if (value > localValue) hasGreater = true;
if (value < localValue) hasLess = true;
});
return hasGreater && !hasLess;
}
// Check if vector clocks are concurrent
isVectorClockConcurrent(remoteVC) {
let hasGreater = false;
let hasLess = false;
remoteVC.forEach((value, nodeId) => {
const localValue = this.vectorClock.get(nodeId) || 0;
if (value > localValue) hasGreater = true;
if (value < localValue) hasLess = true;
});
return hasGreater && hasLess;
}
// Handle conflicts
handleConflict(path, remoteValue, update) {
const localValue = this.getValueAtPath(this.state, path);
switch (this.options.conflictResolution) {
case 'vector-clock':
// Already handled by shouldApplyUpdate
break;
case 'last-write-wins':
if (update.timestamp > this.getLastUpdateTime(path)) {
this.setValueAtPath(this.state, path, remoteValue);
}
break;
case 'custom':
if (this.options.conflictResolver) {
const resolvedValue = this.options.conflictResolver(
localValue,
remoteValue,
update
);
this.setValueAtPath(this.state, path, resolvedValue);
}
break;
}
}
// Sync with peer
syncWithPeer(peerId) {
const portId = `peer-${peerId}`;
this.portManager.sendMessage(portId, {
type: 'sync-request',
data: {
vectorClock: this.serializeVectorClock(),
nodeId: this.nodeId,
},
});
}
// Handle sync request
handleSyncRequest(peerId, data) {
const { vectorClock } = data;
const remoteVC = this.deserializeVectorClock(vectorClock);
// Find state differences
const updates = this.findStateDifferences(remoteVC);
// Send sync response
const portId = `peer-${peerId}`;
this.portManager.sendMessage(portId, {
type: 'sync-response',
data: {
updates,
vectorClock: this.serializeVectorClock(),
nodeId: this.nodeId,
},
});
}
// Handle sync response
handleSyncResponse(peerId, data) {
const { updates } = data;
// Apply updates
updates.forEach((update) => {
this.handleStateUpdate(peerId, update);
});
}
// Find state differences based on vector clock
findStateDifferences(remoteVC) {
const differences = [];
// This is simplified - real implementation would track
// updates with their vector clock timestamps
this.vectorClock.forEach((value, nodeId) => {
const remoteValue = remoteVC.get(nodeId) || 0;
if (value > remoteValue) {
// We have updates the peer doesn't have
// Add relevant state updates to differences
}
});
return differences;
}
// Broadcast to all peers
broadcastToPeers(type, data) {
this.peers.forEach((peer, peerId) => {
const portId = `peer-${peerId}`;
this.portManager.sendMessage(portId, {
type,
data,
});
});
}
// Start sync timer
startSyncTimer() {
this.syncInterval = setInterval(() => {
this.peers.forEach((peer, peerId) => {
this.syncWithPeer(peerId);
});
}, this.options.syncInterval);
}
// Stop sync timer
stopSyncTimer() {
if (this.syncInterval) {
clearInterval(this.syncInterval);
this.syncInterval = null;
}
}
// Merge vector clocks
mergeVectorClock(remoteVC) {
const remote = this.deserializeVectorClock(remoteVC);
remote.forEach((value, nodeId) => {
const localValue = this.vectorClock.get(nodeId) || 0;
this.vectorClock.set(nodeId, Math.max(localValue, value));
});
}
// Serialize vector clock
serializeVectorClock() {
return Array.from(this.vectorClock.entries());
}
// Deserialize vector clock
deserializeVectorClock(serialized) {
return new Map(serialized);
}
// Helper methods
setValueAtPath(obj, path, value) {
const parts = path.split('.');
const last = parts.pop();
let current = obj;
parts.forEach((part) => {
if (!current[part]) current[part] = {};
current = current[part];
});
current[last] = value;
}
getValueAtPath(obj, path) {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (!current || !current[part]) return undefined;
current = current[part];
}
return current;
}
getLastUpdateTime(path) {
// In real implementation, track update times per path
return 0;
}
generateNodeId() {
return `node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
emitChange(path, value, update) {
const event = new CustomEvent('state-change', {
detail: { path, value, update },
});
window.dispatchEvent(event);
}
// Get current state
getState() {
return { ...this.state };
}
// Disconnect from peer
disconnectPeer(peerId) {
this.portManager.closePort(`peer-${peerId}`);
this.peers.delete(peerId);
}
// Cleanup
destroy() {
this.stopSyncTimer();
this.peers.forEach((peer, peerId) => {
this.disconnectPeer(peerId);
});
}
}
// Usage
const stateManager = new PortBasedStateManager({
syncInterval: 2000,
conflictResolution: 'vector-clock',
});
// Create channel for peer connection
const channel = new MessageChannel();
// Connect to peer (in another context)
stateManager.connectToPeer('peer-1', channel.port2);
// Update state
stateManager.updateState('user.name', 'John Doe');
stateManager.updateState('settings.theme', 'dark');
// Listen for state changes
window.addEventListener('state-change', (event) => {
console.log('State changed:', event.detail);
});
Performance Optimization
Efficient Message Passing
class OptimizedPortMessenger {
constructor() {
this.portManager = new PortManager();
this.messageBuffer = new Map();
this.batchSize = 50;
this.batchDelay = 16; // One frame
this.compressionThreshold = 1024; // 1KB
}
// Send message with optimization
sendOptimized(portId, message, options = {}) {
const { priority = 'normal', batch = true, compress = true } = options;
// High priority messages bypass optimization
if (priority === 'high') {
return this.sendDirect(portId, message);
}
// Batch messages if enabled
if (batch) {
return this.batchMessage(portId, message);
}
// Compress large messages
if (compress && this.shouldCompress(message)) {
return this.sendCompressed(portId, message);
}
return this.sendDirect(portId, message);
}
// Send message directly
sendDirect(portId, message) {
return this.portManager.sendMessage(portId, message);
}
// Batch messages
batchMessage(portId, message) {
if (!this.messageBuffer.has(portId)) {
this.messageBuffer.set(portId, []);
// Schedule batch send
setTimeout(() => {
this.flushBatch(portId);
}, this.batchDelay);
}
this.messageBuffer.get(portId).push(message);
// Flush if batch is full
if (this.messageBuffer.get(portId).length >= this.batchSize) {
this.flushBatch(portId);
}
}
// Flush message batch
flushBatch(portId) {
const messages = this.messageBuffer.get(portId);
if (!messages || messages.length === 0) return;
// Send batched messages
this.portManager.sendMessage(portId, {
type: 'batch',
messages: messages,
});
// Clear buffer
this.messageBuffer.delete(portId);
}
// Check if message should be compressed
shouldCompress(message) {
const size = JSON.stringify(message).length;
return size > this.compressionThreshold;
}
// Send compressed message
async sendCompressed(portId, message) {
const compressed = await this.compress(message);
return this.portManager.sendMessage(portId, {
type: 'compressed',
data: compressed,
});
}
// Compress data
async compress(data) {
const json = JSON.stringify(data);
const encoder = new TextEncoder();
const uint8Array = encoder.encode(json);
// Use CompressionStream if available
if ('CompressionStream' in window) {
const stream = new Response(uint8Array).body.pipeThrough(
new CompressionStream('gzip')
);
const compressed = await new Response(stream).arrayBuffer();
return compressed;
}
// Fallback to simple compression
return this.simpleCompress(json);
}
// Simple compression fallback
simpleCompress(str) {
// Simple RLE compression for demo
let compressed = '';
let count = 1;
for (let i = 0; i < str.length; i++) {
if (str[i] === str[i + 1]) {
count++;
} else {
compressed += count > 1 ? count + str[i] : str[i];
count = 1;
}
}
return compressed;
}
// Setup optimized receiver
setupOptimizedReceiver(portId, handler) {
this.portManager.onMessage(portId, async (message) => {
// Handle batched messages
if (message.type === 'batch') {
message.messages.forEach((msg) => handler(msg));
return;
}
// Handle compressed messages
if (message.type === 'compressed') {
const decompressed = await this.decompress(message.data);
handler(decompressed);
return;
}
// Regular message
handler(message);
});
}
// Decompress data
async decompress(data) {
if ('DecompressionStream' in window) {
const stream = new Response(data).body.pipeThrough(
new DecompressionStream('gzip')
);
const decompressed = await new Response(stream).text();
return JSON.parse(decompressed);
}
// Fallback
return this.simpleDecompress(data);
}
// Simple decompression fallback
simpleDecompress(str) {
// Reverse simple RLE compression
let decompressed = '';
let i = 0;
while (i < str.length) {
if (/\d/.test(str[i])) {
let count = '';
while (i < str.length && /\d/.test(str[i])) {
count += str[i++];
}
decompressed += str[i].repeat(parseInt(count));
} else {
decompressed += str[i];
}
i++;
}
return JSON.parse(decompressed);
}
// Create transferable objects
createTransferable(data) {
if (data instanceof ArrayBuffer) {
return { data, transfer: [data] };
}
if (data instanceof ImageBitmap) {
return { data, transfer: [data] };
}
if (typeof data === 'object' && data.buffer instanceof ArrayBuffer) {
return { data: data.buffer, transfer: [data.buffer] };
}
return { data, transfer: [] };
}
// Send with transferables
sendWithTransfer(portId, message, transferables = []) {
// Automatically detect transferables
const autoTransfer = this.detectTransferables(message);
const allTransfer = [...transferables, ...autoTransfer];
return this.portManager.sendMessage(portId, message, allTransfer);
}
// Detect transferable objects
detectTransferables(obj, found = []) {
if (
obj instanceof ArrayBuffer ||
obj instanceof MessagePort ||
obj instanceof ImageBitmap
) {
found.push(obj);
} else if (typeof obj === 'object' && obj !== null) {
Object.values(obj).forEach((value) => {
this.detectTransferables(value, found);
});
}
return found;
}
}
// Usage
const optimized = new OptimizedPortMessenger();
// Send regular message
optimized.sendOptimized('port-1', {
type: 'update',
data: 'small payload',
});
// Send large message (will be compressed)
const largeData = new Array(1000).fill('data').join('');
optimized.sendOptimized('port-1', {
type: 'bulk',
data: largeData,
});
// Send high priority message
optimized.sendOptimized(
'port-1',
{
type: 'critical',
alert: 'System error',
},
{ priority: 'high' }
);
// Send with transferables
const buffer = new ArrayBuffer(1024);
optimized.sendWithTransfer('port-1', {
type: 'binary',
data: buffer,
});
// Setup receiver
optimized.setupOptimizedReceiver('port-1', (message) => {
console.log('Received:', message);
});
Best Practices
-
Always start ports when using addEventListener
port.addEventListener('message', handler); port.start(); // Required!
-
Transfer ports, don't copy
// Good - transfers ownership window.postMessage('init', '*', [port]); // Bad - port becomes unusable window.postMessage({ port }, '*');
-
Close ports when done
// Clean up resources port.close();
-
Validate messages
port.onmessage = (event) => { if (!isValidMessage(event.data)) return; processMessage(event.data); };
Conclusion
The Channel Messaging API enables powerful communication patterns:
- Direct port-to-port communication
- Worker orchestration with dedicated channels
- Secure frame messaging with origin validation
- Distributed state management
- Performance optimization with batching and compression
- Complex messaging patterns
Key takeaways:
- Use MessageChannel for dedicated communication
- Transfer ports for ownership handoff
- Always validate message origins
- Close ports to free resources
- Consider performance optimizations
- Build robust error handling
Create sophisticated multi-context applications with secure, efficient communication!