The Problem
Web applications often need a lightweight way to share state between tabs, workers, or separate Node processes without a server round‑trip. The native BroadcastChannel API is only available in modern browsers, and there is no built‑in equivalent for Node or Deno, leaving developers to write custom IPC logic.
What This Does
broadcast-channel implements a polyfill‑style API that mirrors the browser BroadcastChannel while also supporting Node (via filesystem sockets) and Deno. The core export lives in src/index.js and re‑exports:
BroadcastChannel– class defined insrc/broadcast-channel.jsLeaderElection– class defined insrc/leader-election.jsenforceOptions– helper insrc/options.js
The library selects an appropriate transport at runtime (src/method-chooser.js) based on the environment and optional type option (native, idb, localstorage, node, simulate). Each transport lives in its own file under src/methods/ and implements a minimal create, postMessage, onMessage, and close contract.
How It Is Wired
- Entry point – Consumer code imports
BroadcastChannelfromsrc/index.js. - Construction –
new BroadcastChannel(name, opts)invokes the constructor insrc/broadcast-channel.js. - Method selection – The constructor calls
chooseMethod(name, opts)fromsrc/method-chooser.js. This module imports the five transport modules and picks one based on feature detection and any forcedtype. - Transport creation – The chosen module’s
create(name, opts)returns an object withpostMessage,onMessage, andclose. For Node,src/methods/node.jscreates a temporary folder (ensureBaseFolderExists) and a Unix‑socket file (createDatabase). For browsers,src/methods/indexed-db.jsorsrc/methods/localstorage.jsopen an IndexedDB database (createDatabase) or attach astorageevent listener. - Message flow – Calls to
channel.postMessage(payload)forward to the transport’spostMessage. In Node this writes a line to the socket file; in IndexedDB it stores a record; inlocalstorageit updates a key. Incoming messages are delivered via the transport’sonMessagecallback, which theBroadcastChannelinstance forwards to the user‑providedonmessagehandler or registered listeners. - Leader election –
src/leader-election.jsbuilds on top ofBroadcastChannel. It creates an internal channel (new BroadcastChannel('__leader__')) and uses a simple “heartbeat” protocol (applyOnce,applyRun) to decide which instance holds the leader flag. All leader logic lives in this file and insrc/leader-election-util.js. - External side‑effects – The only I/O is: Filesystem – Node transport writes/reads socket files (
src/methods/node.js). IndexedDB – Browser transport opens a DB (src/methods/indexed-db.js). No network calls are made.
The internal call graph shows $export as the most widely used helper (98 call sites) and validate (30 sites). src/util.js is the most imported module (8 inbound edges) but does not perform side effects, making it a low‑risk change target. The code base has no circular dependencies, simplifying refactoring.
How To Use It
# install the published package (the repo’s README recommends this)
npm install --save broadcast-channel
// src/app.ts – example usage
import { BroadcastChannel, LeaderElection } from 'broadcast-channel';
const bc = new BroadcastChannel('my-topic'); // src/broadcast-channel.js
bc.onmessage = ev => console.log('msg', ev); // transport forwards to onMessage
const le = new LeaderElection('my-topic'); // src/leader-election.js
le.isLeader().then(is => console.log('leader?', is));
No additional build step is required – the published dist/ bundles are generated by the Rollup config in config/rollup.config.mjs, but the source files are directly consumable by modern bundlers. If you need a deterministic transport (e.g., fast unit tests), call enforceOptions({ type: 'simulate' }) from src/options.js before creating any channels.
Real‑World Use
A SaaS dashboard opens a BroadcastChannel('user‑settings') in each tab. When a user updates a setting in one tab, the change is posted via the channel; all other tabs receive the onmessage event and refresh their UI instantly. In a Node worker pool, each process creates new BroadcastChannel('job‑queue'); the leader election component guarantees that only one process runs the cleanup routine at a time.
Code Health & Issues
- HIGH – GitHub Actions not pinned to commit SHA (
.github/workflows/*.yml). - HIGH – No lockfile for
dist/esbrowser/package.json; reproducible builds impossible. - MEDIUM – GITHUB_TOKEN permissions not limited (
.github/workflows/main.yml). - MEDIUM – No dependency‑vulnerability scan in CI.
- MEDIUM – Generated
dist/directory is committed; risk of stale builds. - MEDIUM – Checkout step keeps credentials; should set
persist-credentials: false. - LOW – No job timeout limits in CI workflows.
- LOW – Missing conventional repo files (
.editorconfig, formatter config).
Additional static findings: deep nesting (max depth 9) in src/leader-election.js and related files, an empty catch {} in src/methods/node.js, duplicated blocks across transport implementations, and an oversized integration test (test/integration.test.js). These are documented in the measured analysis and have concrete fix suggestions.
The Bottom Line
broadcast-channel delivers a functional, cross‑environment pub/sub primitive with leader election, backed by well‑isolated transport modules. The codebase is small enough to audit, but hygiene issues (unpinned CI actions, missing lockfile, committed build artifacts) must be addressed before production deployment. It is suitable for teams that need tab‑/process‑level coordination without adding a server component, provided they are comfortable managing the identified technical debt.