single-tab
Version:
A lightweight React library to detect and handle duplicate tabs in web applications
136 lines (135 loc) • 5.23 kB
JavaScript
import { v4 as uuidv4 } from "uuid";
/**
* Manages duplicate tab detection using BroadcastChannel (modern browsers) or sessionStorage (fallback).
*/
export const createTabManager = (appId, onDuplicate) => {
// Generate a unique ID for the current tab
const tabId = typeof window !== "undefined" ? uuidv4() : "";
let channel = null;
/**
* Initializes the TabManager, setting up communication channels and registering the current tab.
*/
const initialize = () => {
if (typeof window === "undefined") {
console.warn("[SingleTab] This library is designed for browser environments only.");
return; // Exit initialization if not in a browser environment
}
if (typeof window !== "undefined") {
if ("BroadcastChannel" in window) {
// Use BroadcastChannel for modern browsers
channel = new BroadcastChannel(appId);
channel.onmessage = (event) => handleMessage(event.data);
}
else if ("addEventListener" in window) {
// Use sessionStorage as a fallback
syncWithSessionStorage();
window.addEventListener("storage", handleStorageEvent);
}
}
// Register the current tab
registerTab();
// Ensure cleanup when the tab is closed
window.addEventListener("beforeunload", unregisterTab);
};
/**
* Handles messages from other tabs via BroadcastChannel.
* @param message - The message containing the type and sender tabId.
*/
const handleMessage = (message) => {
if (message.type === "NEW_TAB" && message.tabId !== tabId) {
// Send a message back to the duplicate tab, telling it to restrict itself
if (channel) {
channel.postMessage({ type: "DUPLICATE_WARNING", tabId: message.tabId });
}
}
else if (message.type === "DUPLICATE_WARNING" && message.tabId === tabId && onDuplicate) {
// Only the duplicate tab (Tab B) should trigger onDuplicate
onDuplicate();
}
};
/**
* Syncs the current tab with sessionStorage to detect duplicate tabs.
*/
const syncWithSessionStorage = () => {
const activeTabs = getActiveTabs();
if (activeTabs.length > 0 && onDuplicate) {
onDuplicate(); // Trigger callback if duplicate tabs exist
}
};
/**
* Handles storage events triggered by updates in sessionStorage.
* @param event - The StorageEvent containing the key and updated value.
*/
const handleStorageEvent = (event) => {
if (event.key === `${appId}_tabs` && onDuplicate) {
syncWithSessionStorage();
}
};
/**
* Retrieves the list of active tabs from sessionStorage.
* @returns An array of active tab IDs.
*/
const getActiveTabs = () => {
const tabs = sessionStorage.getItem(`${appId}_tabs`);
return tabs ? JSON.parse(tabs) : [];
};
/**
* Updates the list of active tabs in sessionStorage.
* @param tabs - The updated list of active tab IDs.
*/
const updateActiveTabs = (tabs) => {
sessionStorage.setItem(`${appId}_tabs`, JSON.stringify(tabs));
};
/**
* Registers the current tab as active by notifying other tabs and updating storage.
*/
const registerTab = () => {
if (channel) {
// Notify other tabs that a new tab has opened
channel.postMessage({ type: "NEW_TAB", tabId });
// Check if there are already active tabs, meaning this is a duplicate
const activeTabs = getActiveTabs();
if (activeTabs.length > 0) {
// Send a self-message to restrict this tab
channel.postMessage({ type: "DUPLICATE_WARNING", tabId });
}
}
else {
const activeTabs = getActiveTabs();
if (activeTabs.length > 0 && onDuplicate) {
onDuplicate(); // Restrict this tab only
}
activeTabs.push(tabId);
updateActiveTabs(activeTabs);
}
};
/**
* Unregisters the current tab by notifying other tabs and cleaning up storage.
*/
const unregisterTab = () => {
if (channel) {
// Notify other tabs via BroadcastChannel
channel.postMessage({ type: "TAB_CLOSED", tabId });
channel.close(); // Safely close the BroadcastChannel
}
else {
// Remove this tab from sessionStorage
const activeTabs = getActiveTabs().filter((id) => id !== tabId);
updateActiveTabs(activeTabs);
}
};
// Initialize the tab manager
initialize();
// Return a cleanup function to allow manual teardown
return {
cleanup: () => {
if (typeof window === "undefined")
return; // No-op during SSR
unregisterTab();
if (!channel) {
window.removeEventListener("storage", handleStorageEvent);
}
window.removeEventListener("beforeunload", unregisterTab);
},
};
};