npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tab-election

v4.3.2

Published

Provides leadership election and communication in the browser across tabs and workers using the Locks API and BroadcastChannel.

Readme

Tab Election

Provides leadership election and communication in the browser across tabs and workers using the Locks API and BroadcastChannel. It works in modern browsers.

The Locks API allows us to have a very reliable leadership election, with virtually no delay in database or server connections and app startup time. When the existing leader is closed, the next tab will become the new leader immediately. The Tab interface allows calls and messages to be queued before a leader is elected and sent afterwards. The Tab interface supports everything you need to have all tabs communicate with one leader for loading, saving, and syncing data between tabs, including calling API methods the leader provides, broadcasting messages to other tabs, and state syncing.

Install

npm install --save tab-election

API

import { Tab } from 'tab-election';

const tab = new Tab();

tab.waitForLeadership(() => {
  // establish websocket, database connection, or whatever is needed as the leader
});

If a tab needs to stop being a leader (or waiting to become one) you can call tab.relinquishLeadership() or the function passed into tab.waitForLeadership((relinquishLeadership) => { }). To completely close all connections with other tabs and allow for garbage collection, call tab.close().

import { Tab } from 'tab-election';

const tab = new Tab('namespace');

tab.waitForLeadership((relinquishLeadership) => {
  // establish websocket, database connection, or whatever is needed as the leader, return an API
  return {
    async loadData() {
      // return await db.load(...);
    },
    letItGo() {
      relinquishLeadership();
    }
  }
});

if (somethingHappens) {
  tab.relinquishLeadership();
}

// ... sometime later, perhaps a tab is stale or goes into another state that doesn't need/want leadership
tab.close();

The tab.waitForLeadership() method can be async. Calls to the leader will be queued while the API is initialized. The waitForLeadership method returns a promise which will resolve with a boolean. If resolved with true, the leadership was relinquished while the tab was the leader. When false, it was relinquished before taking leadership.

import { Tab } from 'tab-election';

const tab = new Tab('namespace');

tab.waitForLeadership(async () => {
  // establish websocket, database connection, or whatever is needed as the leader, return an API
  return {
    async loadData() {
      // return await db.load(...);
    },
  }
}).then(wasLeader => {
  console.log('This tab the current leader:', wasLeader);
}, error => {
  console.error('There was an error initializing the leader API', error);
});

Errors thrown within API methods will be returned to the caller and thrown in that context. E.g. if a tab calls

import { Tab } from 'tab-election';

const tab = new Tab('namespace');

tab.waitForLeadership(async () => {
  // establish websocket, database connection, or whatever is needed as the leader, return an API
  return {
    async loadData() {
      // This exception is forwarded on to the caller to handle
      throw new Error('Cannot load the data');
    },
  }
});

async function loadData() {
  try {
    // This will recieve an error 'Cannot load the data' from the leader and can be handled here
    return await tab.call('loadData');
  } catch(err) {
    console.error('Error loading data from leader', err);
  }
}

To communicate between tabs, send and receive messages.

import { Tab } from 'tab-election';

const tab = new Tab('namespace');

tab.addEventListener('message', event => console.log(event.data));
tab.send('This is a test'); // will not send to self, only to other tabs

To keep state (any important data) between the current leader and the other tabs, use state(). Use this to let the other tabs know when the leader is syncing, whether it is online, or if any errors have occured. state() will return the current state of the leader and state(data) will set the current state if the tab is the current leader.

The state object can contain anything that is supported by the Structured Clone Algorithm including Dates, RegExes, Sets, and Maps.

import { Tab } from 'tab-election';

const tab = new Tab('namespace');

tab.waitForLeadership(() => {
  // establish websocket, database connection, or whatever is needed as the leader
  tab.setState({ connected: false });
  // connect to the server ...
  tab.setState({ connected: true });
});

tab.addEventListener('state', event => console.log('The leader is connected to the server?', event.data.connected));

To allow tabs to call methods on the leader (including the leader), use the call() method. The return result is always asyncronous. The API that is callable should be returned from the waitForLeadership callback. If the leader has established a connection to the server and/or database, this may be used for other tabs to get/save data through that single connection.

import { Tab } from 'tab-election';

const tab = new Tab('namespace');

tab.waitForLeadership(async () => {
  // Can have async instructions here. Calls to `call` in any tab will be queued until the API is returned.
  const db = await connectToTheDatabase();
  return { db };
});

const result = await tab.call('db.saveRecord', { myData: 'foobar' });
if (result === true) {
  console.log('Successfully saved');
}

If a tab wants to make calls to the leader, send and receive messages, and know the state, but it does not want to ever become the leader, then don't call waitForLeadership. This is useful when workers are used for leadership and UI contexts make the requests and display state.

import { Tab } from 'tab-election';

const tab = new Tab('namespace');

const result = await tab.call('saveData', { myData: 'foobar' });
if (result === true) {
  console.log('Successfully saved');
}

Hub & Spoke Architecture

For more complex applications, tab-election provides a Hub & Spoke architecture that simplifies multi-tab coordination with type-safe service registration and RPC communication. The Hub runs in a SharedWorker, WebWorker, or elected tab to manage shared services, while Spokes in each tab communicate with the Hub through typed proxies.

Type-Safe Service Definition

Services must define a namespace property for compile-time safety:

import { Hub, Spoke, Service } from 'tab-election';

// Define a service class with required namespace
class DatabaseService implements Service {
  readonly namespace = 'db';
  hub: Hub | undefined;

  async init(hub: Hub) {
    this.hub = hub;
    this.db = await openDatabase();
  }

  async getUser(id: string): Promise<User> {
    return await this.db.get('users', id);
  }

  async saveUser(user: User): Promise<void> {
    await this.db.put('users', user);
    this.hub?.emit('user-saved', { user }); // Notify all connected tabs
  }
}

class AuthService implements Service {
  readonly namespace = 'auth';

  async login(credentials: LoginData): Promise<Token> {
    // Authentication logic...
  }
}

Hub Setup (in SharedWorker)

const hub = new Hub(hub => {
  hub.register(new DatabaseService());
  hub.register(new AuthService());
});

Spoke Setup (in each tab)

const spoke = new Spoke({
  workerUrl: 'hub.js',
  name: 'app-session',
  version: '1.0.0'
});

// Type-safe service access - fully typed methods and return values
const db = spoke.getService<DatabaseService>('db');
const auth = spoke.getService<AuthService>('auth');
const auth = spoke.getService<AuthService>('wrong'); // ❌ TypeScript error, namespace must match (safety feature)

// All method calls are fully typed
const user = await db.getUser('123');           // Returns Promise<User>
const token = await auth.login(credentials);    // Returns Promise<Token>

// Listen for service events
const unsubscribe = db.on('user-saved', (payload) => {
  console.log('User was updated:', payload.user);
});

Type Safety Benefits

  • Compile-time namespace validation: Impossible to mismatch namespace strings with service classes
  • Full type inference: Method signatures, parameters, and return types are all preserved
  • IntelliSense support: Full autocomplete and type checking in your IDE
  • Refactoring safety: Renaming methods or changing signatures is caught at compile time

The Hub & Spoke pattern is ideal when you need centralized resource management, type-safe inter-tab communication, or want to isolate heavy operations in a worker thread.