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 in src/broadcast-channel.js
  • LeaderElection – class defined in src/leader-election.js
  • enforceOptions – helper in src/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

  1. Entry point – Consumer code imports BroadcastChannel from src/index.js.
  2. Construction – new BroadcastChannel(name, opts) invokes the constructor in src/broadcast-channel.js.
  3. Method selection – The constructor calls chooseMethod(name, opts) from src/method-chooser.js. This module imports the five transport modules and picks one based on feature detection and any forced type.
  4. Transport creation – The chosen module’s create(name, opts) returns an object with postMessage, onMessage, and close. For Node, src/methods/node.js creates a temporary folder (ensureBaseFolderExists) and a Unix‑socket file (createDatabase). For browsers, src/methods/indexed-db.js or src/methods/localstorage.js open an IndexedDB database (createDatabase) or attach a storage event listener.
  5. Message flow – Calls to channel.postMessage(payload) forward to the transport’s postMessage. In Node this writes a line to the socket file; in IndexedDB it stores a record; in localstorage it updates a key. Incoming messages are delivered via the transport’s onMessage callback, which the BroadcastChannel instance forwards to the user‑provided onmessage handler or registered listeners.
  6. Leader election – src/leader-election.js builds on top of BroadcastChannel. 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 in src/leader-election-util.js.
  7. 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.