tab-whisper
v1.0.0
Published
A lightweight, browser-only framework for inter-tab/window communication using the Broadcast Channel API
Maintainers
Readme
Tab Whisper
A lightweight, browser-only framework for inter-tab/window communication using the Broadcast Channel API. Enables each browsing context (tab, window, iframe) to register a unique identity, discover other active contexts, and send messages to specific identities or broadcast messages to all.
Features
- Automatic Identity Management: Each instance generates and maintains a unique identifier
- Dual Identity System: Support for both internal IDs and user-provided registration IDs
- Peer Discovery: Dynamic discovery and tracking of other active contexts
- Flexible Messaging: Support for both broadcast and direct messaging
- Automatic Timestamps: All messages include automatic timestamps
- Reliable Peer Management: Advanced tab close detection and heartbeat system
- Framework Agnostic: Works with any JavaScript framework or vanilla JS
- TypeScript Support: Full TypeScript definitions included
- Browser-Only: No Node.js dependencies or server-side functionality
Installation
npm install tab-whisperBrowser Compatibility
- Chrome 54+
- Firefox 38+
- Safari 15.4+
- Edge 79+
Peer Management
The library includes advanced peer management features to ensure reliable communication:
Tab Close Detection
- Immediate Detection: Uses
beforeunloadevent to detect tab close/refresh instantly - Automatic Cleanup: Sends disconnect messages before tabs close
- Instant Peer Removal: Other tabs immediately remove closing peers
Heartbeat System
- 5-Second Intervals: Sends heartbeat messages every 5 seconds
- 15-Second Timeout: Removes peers that haven't responded for 15 seconds
- Backup Mechanism: Handles cases where
beforeunloaddoesn't fire (crashes, network issues)
Multiple Detection Methods
beforeunloadEvent: Primary method for immediate tab close detectionvisibilitychangeEvent: Detects when tabs become hidden- Heartbeat System: Backup for reliable peer cleanup
Basic Usage
ES Modules
import { TabCommunicator } from 'tab-whisper';
const communicator = new TabCommunicator({
channelName: 'my-app',
registrationId: 'user-dashboard', // Optional user-provided ID
onMessage: (message) => {
console.log('Received:', message);
},
onPeerConnected: (peerId) => {
console.log('Peer connected:', peerId);
},
onPeerDisconnected: (peerId) => {
console.log('Peer disconnected:', peerId);
}
});
console.log('My internal ID:', communicator.id);
console.log('My registration ID:', communicator.registrationId);
console.log('Active peers:', communicator.peers);CommonJS
const { TabCommunicator } = require('tab-whisper');
const communicator = new TabCommunicator({
channelName: 'my-app'
});API Reference
Constructor
new TabCommunicator(options: TabCommunicatorOptions)Options:
channelName(string, required): Identifier for the communication channelregistrationId(string, optional): User-provided identifier for this instanceonMessage(function, optional): Callback for handling incoming messagesonPeerConnected(function, optional): Callback when a new peer connectsonPeerDisconnected(function, optional): Callback when a peer disconnectsonError(function, optional): Callback for error handling
Properties
id(read-only string): Internal ID of current instance (automatically generated)registrationId(read-only string | null): User-provided registration IDpeers(read-only Set): Set of active peer IDs (excluding self)channelName(read-only string): Name of the communication channelisConnected(read-only boolean): Connection status
Methods
send(targetId, type, payload)
Sends a message to a specific peer or broadcasts to all peers.
targetId(string | null): Recipient ID (internal ID or registration ID), or null for broadcasttype(string): Message type identifierpayload(any): JSON-serializable data
// Send to specific peer
communicator.send('peer-id', 'chat', {
text: 'Hello!',
sender: 'User A'
});
// Broadcast to all peers
communicator.send(null, 'update', {
data: { count: 42 }
});on(eventType, callback)
Registers an event listener.
eventType(string): Event type ('message', 'peerConnected', 'peerDisconnected', 'error')callback(function): Handler function
communicator.on('message', (message) => {
console.log(`${message.from}: ${message.payload.text}`);
});off(eventType, callback)
Removes an event listener.
const handler = (message) => console.log(message);
communicator.on('message', handler);
communicator.off('message', handler);close()
Disconnects the instance from the channel and cleans up resources.
communicator.close();Usage Examples
Direct Messaging
import { TabCommunicator } from 'tab-whisper';
const communicator = new TabCommunicator({
channelName: 'chat-app',
registrationId: 'user-123'
});
// Send message to specific peer
communicator.send('user-456', 'chat', {
text: 'Hello there!',
timestamp: Date.now()
});
// Listen for chat messages
communicator.on('message', (message) => {
if (message.type === 'chat') {
console.log(`${message.from}: ${message.payload.text}`);
}
});Broadcast Messaging
const communicator = new TabCommunicator({
channelName: 'my-app'
});
// Broadcast state update to all tabs
communicator.send(null, 'stateUpdate', {
key: 'userPreferences',
value: { theme: 'dark', language: 'en' }
});
// Listen for state updates
communicator.on('message', (message) => {
if (message.type === 'stateUpdate') {
const { key, value } = message.payload;
localStorage.setItem(key, JSON.stringify(value));
}
});Presence Detection
const communicator = new TabCommunicator({
channelName: 'collaboration-app',
registrationId: 'editor-1'
});
const activeUsers = new Set();
communicator.on('peerConnected', (peerId) => {
activeUsers.add(peerId);
updateUserList();
});
communicator.on('peerDisconnected', (peerId) => {
activeUsers.delete(peerId);
updateUserList();
});
function updateUserList() {
console.log('Active users:', Array.from(activeUsers));
// Update UI with active users
}
// The peer management system ensures accurate presence detection:
// - Tabs that close are immediately removed from peer lists
// - Refreshed tabs are detected and re-registered
// - Crashed or unresponsive tabs are cleaned up within 15 secondsError Handling
import {
TabCommunicator,
BroadcastChannelUnsupportedError,
PeerNotFoundError
} from 'tab-whisper';
try {
const communicator = new TabCommunicator({
channelName: 'my-app',
onError: (error) => {
console.error('Communication error:', error);
}
});
// Try to send to a peer that might not exist
try {
communicator.send('unknown-peer', 'test', { data: 'hello' });
} catch (error) {
if (error instanceof PeerNotFoundError) {
console.log('Peer not found, sending as broadcast instead');
communicator.send(null, 'test', { data: 'hello' });
}
}
} catch (error) {
if (error instanceof BroadcastChannelUnsupportedError) {
console.error('BroadcastChannel not supported in this browser');
// Provide fallback or show error message
}
}Framework Integration
React Hook
import { useEffect, useState } from 'react';
import { TabCommunicator } from 'tab-whisper';
function useTabCommunicator(channelName, registrationId) {
const [communicator, setCommunicator] = useState(null);
const [peers, setPeers] = useState(new Set());
const [messages, setMessages] = useState([]);
useEffect(() => {
const comm = new TabCommunicator({
channelName,
registrationId,
onMessage: (message) => {
setMessages(prev => [...prev, message]);
},
onPeerConnected: (peerId) => {
setPeers(prev => new Set([...prev, peerId]));
},
onPeerDisconnected: (peerId) => {
setPeers(prev => {
const newPeers = new Set(prev);
newPeers.delete(peerId);
return newPeers;
});
}
});
setCommunicator(comm);
return () => {
comm.close();
};
}, [channelName, registrationId]);
return { communicator, peers, messages };
}
// Usage in component
function ChatComponent() {
const { communicator, peers, messages } = useTabCommunicator('chat', 'user-1');
const sendMessage = (text) => {
if (communicator) {
communicator.send(null, 'chat', { text });
}
};
return (
<div>
<div>Active peers: {peers.size}</div>
<div>Messages: {messages.length}</div>
</div>
);
}Message Structure
All messages automatically include:
interface Message {
from: string; // Sender ID (internal or registration ID)
to: string | null; // Recipient ID or null for broadcast
type: string; // Message type
payload: any; // Message data
timestamp: number; // Automatically added timestamp
}Internal Communication
The library uses internal message types for peer management:
__tc_register: Peer registration messages__tc_disconnect: Peer disconnection messages__tc_discover: Peer discovery requests__tc_discover_response: Peer discovery responses__tc_heartbeat: Heartbeat messages for peer verification__tc_heartbeat_response: Heartbeat responses
Error Types
BroadcastChannelUnsupportedError: BroadcastChannel API not supportedInvalidMessageError: Invalid message data or typePeerNotFoundError: Target peer not foundCommunicatorClosedError: Attempting to use closed communicator
TypeScript Support
The package includes full TypeScript definitions:
import {
TabCommunicator,
TabCommunicatorOptions,
Message,
EventType
} from 'tab-whisper';
const options: TabCommunicatorOptions = {
channelName: 'typed-app',
registrationId: 'user-1',
onMessage: (message: Message) => {
console.log(message.type, message.payload);
}
};
const communicator = new TabCommunicator(options);License
MIT
Contributing
Contributions are welcome! Please ensure all changes maintain backward compatibility and follow the established patterns.
