browser-tab-elect
v1.0.3
Published
Tab-elect solves the problem of only wanting one browser tab to run a job, and ensure that there is always one browser tab running it even if the previous running tab was closed. This package takes care of the inter-tab communication required to elect one
Maintainers
Readme
browser-tab-elect
Elect a single browser tab as the leader using BroadcastChannel + localStorage fencing. Only the leader performs critical work (e.g., triggering downloads), with automatic failover if the leader tab closes or crashes.
- Headless core API for any framework
- React hook
useSingleTabLeader - Dual ESM/CJS builds with TypeScript types
Install
npm i browser-tab-electCore API
import { createLeaderElector } from 'browser-tab-elect';
const elector = createLeaderElector();
elector.start();
if (elector.isLeader()) {
// perform leader-only action
}
const unsubscribe = elector.subscribe((state) => {
console.log('leader?', state.isLeader, 'tabId', state.tabId, 'epoch', state.epoch);
});
// later
unsubscribe();
elector.stop();Notes
start()is idempotent. Calling it multiple times is safe and will not attach duplicate listeners or timers.stop()fully cleans up timers, storage listeners, and channel listeners; you can safely callstart()again later.- If the leader record in
localStorageis removed, remaining tabs will automatically run a fresh election.
Options
createLeaderElector({
storageKey: 'citadel:leader', // localStorage key
channelName: 'citadel_leadership', // BroadcastChannel name
leaseMs: 8000, // leader lease duration
renewEveryMs: 3000, // heartbeat interval
electionMinBackoffMs: 80, // election backoff window
electionMaxBackoffMs: 200,
});React Hook
import { useSingleTabLeader } from 'browser-tab-elect/react';
function Component() {
const { isLeader } = useSingleTabLeader();
// gate work
useEffect(() => {
if (!isLeader) return;
// leader-only logic
}, [isLeader]);
return <div>{isLeader ? 'Leader' : 'Follower'}</div>;
}Options and identity
useSingleTabLeader(options) reuses (or creates) an elector instance keyed by the full option set. Changing any of these will create and start a new elector instance:
storageKey,channelNameleaseMs,renewEveryMselectionMinBackoffMs,electionMaxBackoffMs
This ensures the hook reflects configuration changes deterministically without leaking prior listeners.
Protocol (high-level)
- Each tab has a UUID and competes to lead when there's no valid leader.
- Leader writes
{tabId, epoch, leaseUntil}to localStorage and renewsleaseUntilevery ~3s. - Followers accept the freshest epoch with a live lease and wait.
- If the lease expires or the leader steps down, a new election runs. Winner is deterministic: lexicographically highest
tabIdamong candidates. - Fencing: leader actions must verify the record matches their
tabIdand has a live lease.
SSR and Fallbacks
- APIs are safe to import server-side; they no-op when
windowis not available. - If
BroadcastChannelis missing, election still functions via storage reads/writes (less responsive).
Exports
- Root: core API
- Subpath
react: React hook
{
"exports": {
".": { "import": "./dist/index.mjs", "require": "./dist/index.js" },
"./react": { "import": "./dist/react/index.mjs", "require": "./dist/react/index.js" }
}
}License
ISC
