Shared WebSocket Transport. Exactly one context (leader) opens a real WebSocket; other contexts attach via BroadcastChannel and delegate sends, while all contexts receive inbound messages.
One physical WebSocket per {url, scope}. Leader tab owns transport. Other tabs attach and delegate send; all tabs receive messages.
This repository is a package scaffold. Build before use:
- npm i -D tsup typescript
- npm run build
Then import from dist/index.js (ESM) or dist/index.cjs (CJS).
import SharedWsTransport from 'broadcast-websocket';
const transport = new SharedWsTransport('wss://example/ws');
transport.addEventListener('transport_open', () => console.log('open'));
transport.addEventListener('message', (event) => {
const detail = event as CustomEvent<{ data: string | ArrayBuffer }>;
console.log('message', detail.detail.data);
});
transport.addEventListener('transport_close', (event) => {
const detail = event as CustomEvent<{ code: number; reason: string; wasClean: boolean }>;
console.log('close', detail.detail);
});
transport.send(JSON.stringify({ hello: 'world' }));Create a SharedWsTransport in each browsing context (page, window, embedded view). The leader opens the socket; the rest delegate send() via BroadcastChannel and receive messages broadcast by the leader. send() throws unless transportState === 'open'.
Methods:
send(data)terminate(code?, reason?)detach()/dispose()status()→{ id, role, leaderId, transportState, url, scope }
State:
transportState:'connecting' | 'open' | 'closing' | 'closed'role:'leader' | 'follower'
Events:
transport_opentransport_close→{ code, reason, wasClean }transport_error→{ error?: unknown }message→{ data }role_change→{ role, leaderId }
type Options = {
scope?: string; // defaults to URL origin
protocols?: string | string[];
heartbeatMs?: number;
timeoutMs?: number;
debug?: boolean;
logger?: (message: string, detail?: Record<string, unknown>) => void;
};
Local echo demos:
- Install dev deps:
npm i -D tsup typescript - Build:
npm run build - Install the demo server dependency:
npm i ws - Start the demo server:
npm run demo:server(listens onws://localhost:8787) - Serve the folder (any static server) and open:
demo/simple.htmlin two windows or side-by-side browser contextsdemo/frames.html(two embedded clients on one page)
- Type messages. The leader connects to the local server; follower sends are delegated via BroadcastChannel; inbound messages are broadcast to followers.
React demo (Vite + Tailwind):
- Build the library once so the local file dependency has
dist/:pnpm build - Install demo deps and link the local package:
pnpm -C demo/react install - Start the echo server:
pnpm demo:server(listens onws://localhost:8787) - Run the React demo:
pnpm demo:react(then open the shown URL; open multiple windows to see leader/follower behavior)
Embedded demo (single page with two client panes):
- Build:
npm run build - Start the demo server:
npm run demo:server - Serve the folder and open
demo/frames.html(it loads twopane.htmlclient panes) - Try sending from either pane; one becomes leader, the other follows.
- Requires
BroadcastChannel(delegation/broadcast) andlocalStorage(election) to coordinate between contexts. - Browser WebSocket API does not expose native ping/pong. If you need keepalives, implement app-level pings.
- No buffering/queues:
send()throws unless the leader transport is open.
- Build (ESM + CJS + types):
npm run build - Dry-run publish:
npm publish --dry-run
Note: Only dist/ is published (see package.json#files); demos and docs stay out of the npm tarball.
Exports
- ESM:
dist/index.js - CJS:
dist/index.cjs - Types:
dist/index.d.ts
MIT