mrkeyoor.com_
Sat 08 Aug 22:54 UTC
npmUtilsupdated 08 Aug 2026

broadcast-channel

broadcast-channel sends small messages between browsing contexts or Node processes that open the same named channel. It prefers the native BroadcastChannel API, then falls back to IndexedDB or localStorage in browsers; Node uses filesystem sockets, and tests can select an in-process simulator. It also includes leader election, using Web Locks when available and a message-based algorithm otherwise, so one tab can own work such as a server connection. The API resembles the browser standard but is not a polyfill: payloads must be JSON-serializable and handlers receive the posted data directly, not a MessageEvent.

Verdict

The strongest install when cross-tab fallbacks and leader election are both real requirements. Prefer native BroadcastChannel for modern-browser notifications, and move to Redis, WebSockets, or another broker when delivery, trust, throughput, or machine boundaries matter.

API stability4/5The core constructor, postMessage(), onmessage, event listeners, close(), and leader-elector shape have been established across several releases, with declarations and both ESM and CommonJS entry points. Major versions have made meaningful semantic changes: version 5 changed hasLeader into an async function and adopted Web Locks, version 6 added Deno, and version 7 stopped emitting messages that predate channel creation. Pinning a major and reading its changelog is still necessary.
Docs4/5The README covers transport selection, typed messages, event listeners, cleanup, test simulation, Node temporary folders, IndexedDB closure recovery, leader election, duplicate leaders, and explicit non-goals. That is enough to avoid most production mistakes. Weak spots remain: some wording and examples are dated, the hosted page largely mirrors the README, the changelog currently stops at 7.3.0 while npm is 7.4.0, and lower-level option defaults require reading source.
Maintenance5/5Version 7.4.0 was published on August 3, 2026, the repository was pushed on August 7, 2026, and the current development stack tests Node, browsers, end-to-end behavior, ESM, CommonJS, and typings. GitHub reports 11 open issues and PRs, a modest queue for a multi-runtime transport library. Releases in 2025 and 2026 include leader-election correctness fixes rather than dependency-only churn.
Ecosystem4/5The package recorded 3,132,218 downloads in the latest npm week and the repository has 1,999 stars. It supports native browser channels, IndexedDB, localStorage, workers, iframes, Node filesystem sockets, Deno, TypeScript, ESM, and CommonJS, making it unusually portable. It remains intentionally local and JSON-only, with no broker adapters, persistence protocol, authentication layer, or cross-host transport, so it does not replace a messaging ecosystem.

Use it if

  • Several same-origin tabs, frames, or workers need low-latency notifications and you must support browsers without native BroadcastChannel
  • A few Node processes on one machine need light coordination without operating Redis, a broker, or a network service
  • You need one active tab to own a WebSocket, polling loop, or scheduled task and can tolerate leader re-election
  • You want the same typed channel API in browsers, Node, and Deno, plus a fast deterministic transport for tests
Skip it if

Setup reality

npm install broadcast-channel is enough and version 7.4.0 ships conditional browser, Node, and Deno entry points plus TypeScript declarations. There are no native builds, credentials, or required config files, but runtime behavior depends heavily on the selected transport. Modern browsers use native BroadcastChannel; older environments fall back to IndexedDB, then localStorage. Setting webWorkerSupport to false removes IndexedDB from selection and can improve performance, but it also means workers cannot participate. IndexedDB can close when storage is cleared or on Mobile Safari; supply idb.onclose, close the dead channel, and create a fresh instance. Payloads must be JSON-serializable, listeners receive the data itself rather than event.data, a sender should not expect its own message back, and channels do not replay history. Always await close(), especially in tests, because it waits for pending sends and releases transport resources. Node uses filesystem sockets and temporary folders with a two-minute default TTL and a 2,048-write default parallel cap. Large test suites can accumulate folders, so call clearNodeFolder() at suite start; production code should not casually delete coordination state while other processes are live. The simulate method is much faster in tests but stays inside one JavaScript process. enforceOptions() is global, overrides constructor options for every channel, and must be reset after each suite to avoid contaminating unrelated tests. Leader election starts lazily when awaitLeadership() is called, uses Web Locks where available, and remains best-effort: implement onduplicate and make leader work idempotent. None of the transports turns this into cross-host messaging or a durable queue.

Patterns

Send and receive typed messagessend-typed-message

import { BroadcastChannel } from 'broadcast-channel'

type AppMessage =
  | { type: 'signed-out' }
  | { type: 'profile-updated'; userId: string }

const channel = new BroadcastChannel<AppMessage>('app-session')
channel.onmessage = (message) => {
  if (message.type === 'signed-out') redirectToLogin()
}

await channel.postMessage({ type: 'profile-updated', userId: 'u_42' })

The handler receives the posted value directly, not a MessageEvent. Payloads must be JSON-serializable even when TypeScript accepts a broader shape.

Add and remove independent listenersmanage-multiple-listeners

import { BroadcastChannel } from 'broadcast-channel'

const channel = new BroadcastChannel('inventory')
const refresh = (message) => updateInventory(message)
const audit = (message) => console.debug(message)

channel.addEventListener('message', refresh)
channel.addEventListener('message', audit)

channel.removeEventListener('message', audit)

Assigning onmessage again replaces the previous onmessage handler. addEventListener is the right API when several consumers coexist.

Wait for pending sends before shutdownclose-channel

const channel = new BroadcastChannel('jobs')

try {
  await channel.postMessage({ type: 'job-finished', id: job.id })
} finally {
  await channel.close()
}

console.log(channel.isClosed)

close() waits for in-flight sends and transport cleanup. postMessage() after closure throws and includes the serialized message in its error text.

See which transport was selectedinspect-selected-transport

const channel = new BroadcastChannel('diagnostics')

console.log({
  channel: channel.name,
  transport: channel.type,
  closed: channel.isClosed,
})

Typical values are native, idb, localstorage, node, or simulate. Selection varies by runtime and constructor options.

Recreate a channel after IndexedDB closesrecover-indexeddb-close

let channel

function openChannel() {
  channel = new BroadcastChannel('sync', {
    idb: {
      onclose: async () => {
        await channel.close()
        openChannel()
      },
    },
  })
  channel.onmessage = handleSync
}

openChannel()

Unexpected IndexedDB closure is seen most often on Mobile Safari or after users clear storage. Close the old wrapper before opening its replacement.

Skip IndexedDB when workers are irrelevantdisable-worker-support

const channel = new BroadcastChannel('tab-only-events', {
  webWorkerSupport: false,
})

channel.onmessage = handleEvent

This can reduce overhead, but method selection removes IndexedDB and may fall back to localStorage when native BroadcastChannel is unavailable. Workers then cannot join.

Force the fast in-process test transportsimulate-in-tests

import { enforceOptions } from 'broadcast-channel'

beforeEach(() => {
  enforceOptions({ type: 'simulate' })
})

afterEach(() => {
  enforceOptions(null)
})

enforceOptions is global and overrides every constructor option. The simulate transport communicates only inside the current JavaScript process.

Clear Node transport folders before testsclean-node-test-folders

import { clearNodeFolder } from 'broadcast-channel'

beforeAll(async () => {
  const cleared = await clearNodeFolder()
  console.log({ cleared })
})

The promise returns true on the Node transport and false in browsers. Use this for isolated test setup, not while live production processes share channels.

Run one task in the elected tabelect-tab-leader

import { BroadcastChannel, createLeaderElection } from 'broadcast-channel'

const channel = new BroadcastChannel('server-connection')
const elector = createLeaderElection(channel)

await elector.awaitLeadership()
const connection = connectToServer()

Election is lazy and starts when awaitLeadership() is called. Closing a tab normally releases leadership, but leader work should still be idempotent.

Allow more time for throttled peerstune-leader-election

const elector = createLeaderElection(channel, {
  responseTime: 1000,
  fallbackInterval: 2000,
})

await elector.awaitLeadership()

Increase responseTime when multiple leaders appear under slow or timer-throttled environments. Larger values also make failover slower.

Detect and contain duplicate leadershandle-duplicate-leaders

const elector = createLeaderElection(channel)

elector.onduplicate = async () => {
  stopExclusiveWork()
  await elector.die()
  reportDuplicateLeader()
}

await elector.awaitLeadership()
startIdempotentWork()

The README documents rare duplicate leaders during CPU saturation and browser timer throttling. Detection is not a substitute for server-side idempotency or locking.

Give up leadership cleanlyrelease-leadership

if (elector.isLeader) {
  await stopLeaderOnlyResources()
}

await elector.die()
await channel.close()

After die(), pending calls to awaitLeadership() on that elector will no longer resolve. Create a new elector if this process may compete again later.

Alternatives

PackageRegistryPick it when
@vueuse/corenpmA Vue application only needs a reactive wrapper around the browser's native BroadcastChannel API
redux-state-syncnpmYour actual task is synchronizing selected Redux actions and state across browser tabs
redisnpmNode processes run on multiple hosts or need higher-rate pub/sub through an operated server
wsnpmBrowsers or devices need server-mediated real-time messages beyond one origin and one machine