Drop-in WebSocket replacement that shares a single connection across browser tabs. Only one tab maintains the real WebSocket connection — all others send and receive through it transparently.
Every new WebSocket(url) opens a separate TCP connection. If a user has your app open in 5 tabs, that's 5 connections to the same server doing the same thing. shared-socket uses the Web Locks API to elect a single leader tab that holds the real connection, and BroadcastChannel to relay events to all tabs.
npm install shared-socketReplace WebSocket with SharedWebSocket. The API is the same.
import { SharedWebSocket } from 'shared-socket';
// Instead of: const ws = new WebSocket('wss://example.com/feed');
const ws = new SharedWebSocket('wss://example.com/feed');
ws.onopen = () => {
console.log('connected');
ws.send('hello');
};
ws.onmessage = (event) => {
console.log('received:', event.data);
};
ws.onclose = (event) => {
console.log('closed:', event.code, event.reason);
};
ws.onerror = () => {
console.log('error');
};addEventListener works too:
ws.addEventListener('message', (event) => {
console.log(event.data);
}); Tab B (follower) Tab A (leader) Tab C (follower)
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ SharedWebSocket │ │ SharedWebSocket │ │ SharedWebSocket │
│ │ │ │ │ │
│ onopen │ │ Real WS │ │ onopen │
│ onmessage ├────┼──► connection ◄─┼────┤ onmessage │
│ onclose │ │ │ │ │ onclose │
│ onerror │ │ │ │ │ onerror │
└─────────────────┘ └────────┼────────┘ └─────────────────┘
BroadcastChannel │ BroadcastChannel
│
┌────▼────┐
│ Server │
└─────────┘
events: leader ──► all tabs
send(): any tab ──► leader ──► server
-
The first tab to create a
SharedWebSocketfor a given URL acquires an exclusive Web Lock and becomes the leader. It opens the realWebSocketconnection. -
Subsequent tabs for the same URL enter the lock queue and become followers. They communicate with the leader through a
BroadcastChannel. -
When the leader's WebSocket receives a message, it broadcasts the event to all followers. Every tab fires
onmessage. -
When any tab calls
send(), the message reaches the real WebSocket — either directly (leader) or via BroadcastChannel relay (follower). -
If the leader tab closes, the browser releases the lock. The next tab in the queue automatically becomes the new leader and reconnects.
SharedWebSocket implements the same interface as the native WebSocket.
new SharedWebSocket(url: string | URL, protocols?: string | string[])| Property | Type | Description |
|---|---|---|
url |
string |
The URL passed to the constructor |
readyState |
number |
Connection state (CONNECTING, OPEN, CLOSING, CLOSED) |
protocol |
string |
Subprotocol selected by the server |
extensions |
string |
Always '' (v1) |
bufferedAmount |
number |
Always 0 (v1) |
binaryType |
BinaryType |
Accepted but ignored (v1) |
SharedWebSocket.CONNECTING // 0
SharedWebSocket.OPEN // 1
SharedWebSocket.CLOSING // 2
SharedWebSocket.CLOSED // 3Sends data through the WebSocket connection. Throws DOMException with InvalidStateError if readyState is not OPEN.
Closes this instance only. Other tabs are unaffected. If this tab is the leader, the next tab takes over and reconnects.
| Event | Handler | Event Type | Description |
|---|---|---|---|
open |
onopen |
Event |
Connection established |
message |
onmessage |
MessageEvent |
Message received (access data via event.data) |
error |
onerror |
Event |
Connection error |
close |
onclose |
CloseEvent |
Connection closed (access code, reason, wasClean) |
Each unique URL gets its own independent shared connection. Two different URLs = two separate leader elections, two separate WebSocket connections.
const feed = new SharedWebSocket('wss://example.com/feed');
const chat = new SharedWebSocket('wss://example.com/chat');
// These are completely independentWhen the leader tab is closed or crashes:
- The browser releases the Web Lock
- The next tab in the queue becomes the new leader
- The new leader opens a fresh WebSocket connection
- All remaining tabs receive
onopenwhen the new connection is established
This happens automatically with no application code needed.
Calling close() on one tab does not affect other tabs. If the leader calls close(), it releases leadership and the next tab reconnects.
// Tab A
const ws = new SharedWebSocket('wss://example.com/feed');
ws.close(); // Only Tab A is closed. Tab B continues normally.If readyState is not OPEN, send() throws an InvalidStateError. There is no message buffering during leader transitions.
If a tab creates a SharedWebSocket after the connection is already open, it receives the current state from the leader and fires onopen immediately.
- Web Locks API (Chrome 69+, Firefox 96+, Safari 15.4+)
- BroadcastChannel (Chrome 54+, Firefox 38+, Safari 15.4+)
- WebSocket (all modern browsers)
- String messages only — no
BloborArrayBuffersupport extensionsalways returns''bufferedAmountalways returns0binaryTypeis accepted but ignored- No message buffering during leader transitions —
send()throws if not connected readyStateon follower tabs is eventually consistent (synced via BroadcastChannel, not instant)
# Install dependencies
bun install
# Type check
bun run typecheck
# Run unit tests
bun test tests/unit
# Lint
bun run lint
# Build
bun run buildMIT