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

tabus-js

v0.3.0

Published

Type-safe cross-tab message bus using BroadcastChannel, with in-memory fallback.

Downloads

1,109

Readme

tabus-js

Type-safe cross-tab message bus for the browser, built on the native BroadcastChannel API.

npm license types

📖 Documentation — Full docs, examples, and API reference.

Why

When a user signs out in one tab, other open tabs keep showing sensitive data.
tabus-js solves this by letting tabs broadcast events to each other instantly — no server, no WebSockets, no polling.

Installation

Install tabus-js using your preferred package manager.

# npm
npm install tabus-js

# pnpm
pnpm add tabus-js

# yarn
yarn add tabus-js

# bun
bun add tabus-js

Framework compatibility

tabus-js is framework-agnostic. It works in any environment that runs JavaScript in the browser.

| Environment | Supported | | --------------------- | ----------- | | Vanilla JS | ✅ | | React | ✅ | | Vue | ✅ | | Angular | ✅ | | Svelte | ✅ | | Next.js (client only) | ✅ | | Nuxt (client only) | ✅ | | Node.js / SSR | ⚠️ fallback |

SSR note: BroadcastChannel is a browser API — it does not exist on the server. In SSR environments (Next.js, Nuxt, SvelteKit), create the Tabus instance only on the client side. If BroadcastChannel is unavailable, tabus-js falls back to an in-memory bus automatically and emits a console.warn.

Quick start

import { Tabus } from "tabus-js";

type MyEvents = {
  logout: { userId: number };
  ping: { ts: number };
};

const bus = new Tabus<MyEvents>("my-app");

// Listen for events from other tabs
bus.on("logout", ({ userId }) => {
  console.log("User logged out:", userId);
  redirectToLogin();
});

// Broadcast to all other tabs
bus.emit("logout", { userId: 42 });

// Clean up
bus.destroy();

Examples

Vanilla JS

import { Tabus } from "tabus-js";

const bus = new Tabus("my-app");

bus.on("notification", ({ message }) => {
  alert(message);
});

document.querySelector("#btn").addEventListener("click", () => {
  bus.emit("notification", { message: "Hello from this tab!" });
});

React

import { useEffect } from "react";
import { Tabus } from "tabus-js";

type AppEvents = {
  logout: { userId: number };
};

const bus = new Tabus<AppEvents>("my-app");

function App() {
  useEffect(() => {
    const handler = ({ userId }: { userId: number }) => {
      console.log("Logged out:", userId);
      redirectToLogin();
    };

    bus.on("logout", handler);
    return () => bus.off("logout", handler);
  }, []);

  const handleLogout = () => {
    bus.emit("logout", { userId: 42 });
  };

  return <button onClick={handleLogout}>Logout</button>;
}

Vue

<script setup lang="ts">
import { onMounted, onUnmounted } from "vue";
import { Tabus } from "tabus-js";

type AppEvents = {
  logout: { userId: number };
};

const bus = new Tabus<AppEvents>("my-app");

const handler = ({ userId }: { userId: number }) => {
  console.log("Logged out:", userId);
  redirectToLogin();
};

onMounted(() => bus.on("logout", handler));
onUnmounted(() => bus.off("logout", handler));

const handleLogout = () => bus.emit("logout", { userId: 42 });
</script>

<template>
  <button @click="handleLogout">Logout</button>
</template>

Next.js (client only)

"use client";

import { useEffect } from "react";
import { Tabus } from "tabus-js";

type AppEvents = {
  logout: { userId: number };
};

// Create outside the component to share across renders
const bus = new Tabus<AppEvents>("my-app");

export function LogoutButton() {
  useEffect(() => {
    const handler = ({ userId }: { userId: number }) => {
      redirectToLogin();
    };

    bus.on("logout", handler);
    return () => bus.off("logout", handler);
  }, []);

  return (
    <button onClick={() => bus.emit("logout", { userId: 42 })}>Logout</button>
  );
}

Real-world: sync logout across tabs

import { Tabus } from "tabus-js";

type AuthEvents = {
  logout: { userId: number };
  sessionExpired: { reason: string };
};

const auth = new Tabus<AuthEvents>("auth");

// In your auth service
auth.on("logout", () => {
  clearLocalStorage();
  redirectToLogin();
});

auth.on("sessionExpired", ({ reason }) => {
  showToast(`Session expired: ${reason}`);
  redirectToLogin();
});

// When the user clicks logout
function logout(userId: number) {
  auth.emit("logout", { userId });
}

Real-world: sync cart across tabs

import { Tabus } from "tabus-js";

type CartEvents = {
  itemAdded: { productId: string; qty: number };
  itemRemoved: { productId: string };
  cleared: Record<string, never>;
};

const cart = new Tabus<CartEvents>("cart");

cart.on("itemAdded", ({ productId, qty }) => {
  updateCartUI(productId, qty);
});

cart.on("cleared", () => {
  resetCartUI();
});

// When user adds an item
function addToCart(productId: string, qty: number) {
  cart.emit("itemAdded", { productId, qty });
}

API

new Tabus<Events>(channelName?)

Creates a new instance. All instances with the same channelName share a channel.
Defaults to 'tabus' if no name is provided.

bus.on(event, handler)

Subscribes to an event. Returns this for chaining.
Also accepts internal events: tab:join and tab:leave.

bus.emit(event, payload)

Broadcasts an event to all other tabs on the same channel.
The emitting tab does not receive its own events.

bus.off(event, handler)

Removes a previously registered handler. Returns this for chaining.

bus.destroy()

Closes the channel, emits tab:leave, and removes all handlers. Safe to call multiple times.

bus.tabId

A unique UUID identifying this tab instance. Read-only.

Throttle

Limit how often messages are sent to the channel. Useful for high-frequency events like mousemove, scroll, or real-time input sync.

// Leading + trailing (default): first and last events always arrive
const bus = new Tabus('canvas', { throttle: 16 })

// Leading only: first event arrives, intermediates discarded
const bus = new Tabus('canvas', { throttle: 16, trailing: false })

window.addEventListener('mousemove', (e) => {
  bus.emit('cursor:moved', { x: e.clientX, y: e.clientY })
})

Leading edge: the first call in a throttle window fires immediately. Trailing edge: if more calls arrive within the window, the last one fires when the window expires — guaranteeing the final state always reaches other tabs.

| Option | Type | Default | Description | |--------|------|---------|-------------| | throttle | number | 0 | Minimum ms between emitted messages. | | trailing | boolean | true | Emit the last pending message after the window expires. |

Lifecycle events

bus.on("tab:join", ({ tabId }) => console.log("Tab joined:", tabId));
bus.on("tab:leave", ({ tabId }) => console.log("Tab left:", tabId));

These are emitted automatically — you cannot emit them manually.

Fallback

If BroadcastChannel is not available (old browsers, some WebViews, SSR), tabus-js falls back to an in-memory bus automatically. Events will still work within the same tab. A console.warn is emitted once per channel to notify you.

Browser support

| Browser | Version | | ------- | ------- | | Chrome | 54+ | | Firefox | 38+ | | Safari | 15.4+ | | Edge | 79+ |

License

MIT © Rody Huancas