A lightweight, TypeScript-first library for communication between browser tabs. This library enables seamless interaction between multiple open tabs/windows of your application using SharedWorker.
- Type-Safe: Full TypeScript support with extensive type definitions
- Framework Agnostic: Core functionality works with any JavaScript framework
- React Integration: Built-in React hooks and context for easy integration
- Action-Based Communication: Define and execute actions across tabs
- Observable Pattern: Custom observable implementation (no RxJS dependency)
- Heartbeat System: Automatic detection of closed or disconnected tabs
- Minimal Dependencies: Zero external runtime dependencies
- Extensible: Easy to add custom action handlers and extend functionality
pnpm add @powertoys/relay
npm install @powertoys/relay
yarn add @powertoys/relay
// handlers/worksheet-handlers.ts
import { ActionHandler } from "@powertoys/relay";
export interface SelectRangePayload {
address: string;
sheetName?: string;
}
const selectRange: ActionHandler<SelectRangePayload, void> = async payload => {
const { address, sheetName } = payload;
// Implementation...
};
export const worksheetHandlers = {
SELECT_RANGE: selectRange,
};
// handlers/workbook-handlers.ts
export interface WorkbookInfo {
name: string;
sheets: string[];
}
const getWorkbookInfo: ActionHandler<void, WorkbookInfo> = async () => {
// Implementation...
return {
name: "Example Workbook",
sheets: ["Sheet1", "Sheet2"],
};
};
export const workbookHandlers = {
GET_WORKBOOK_INFO: getWorkbookInfo,
};
// Function to generate tab info (optional)
async function generateTabInfo() {
const randomId = `tab_${Math.random().toString(36).slice(2, 9)}`;
return {
tabId: randomId,
tabName: `Document ${randomId.slice(-4)}`,
};
}
// Create the relay instance
export const relay = new Relay({
handlers: {
...worksheetHandlers,
...workbookHandlers,
},
getTabInfo: generateTabInfo,
});
// App.tsx
function App() {
return (
<RelayProvider relay={relay}>
<Dashboard />
</RelayProvider>
);
}
// Dashboard.tsx
function Dashboard() {
const tabInfo = useTabInfo();
const tabList = useTabList();
const requestAction = useRequestAction();
const getWorkbookInfo = async tabId => {
try {
const info = await requestAction<WorkbookInfo>(tabId, "GET_WORKBOOK_INFO");
console.log("Workbook info:", info);
} catch (error) {
console.error("Error:", error);
}
};
return (
<div>
<h1>Current Tab: {tabInfo?.tabName}</h1>
<h2>Connected Tabs:</h2>
<ul>
{tabList.map(tab => (
<li key={tab.tabId}>
{tab.tabName}
{tab.tabId !== tabInfo?.tabId && <button onClick={() => getWorkbookInfo(tab.tabId)}>Get Info</button>}
</li>
))}
</ul>
</div>
);
}
import { Relay } from "@powertoys/relay";
const relay = new Relay({
handlers: {
PING: async () => "PONG",
},
getTabInfo: async () => ({ tabId: "unique-id", tabName: "My Document" }),
});
// Initialize
await relay.initialize();
// Subscribe to events
relay.onEvent({
next: event => {
if (event.type === "tabListUpdated") {
console.log("Connected tabs:", event.tabs);
}
},
});
// Request an action from another tab
const targetTabId = "another-tab-id";
try {
const result = await new Promise((resolve, reject) => {
relay.requestAction(targetTabId, "SOME_ACTION", { data: "example" }, resolve, reject);
});
console.log("Result:", result);
} catch (error) {
console.error("Error:", error);
}
// Clean up when done
relay.cleanup();
The core class for managing tab-to-tab communication.
constructor(options: RelayOptions)
Options:
-
handlers
(Record<string, ActionHandler>): Map of action handlers -
getTabInfo
(() => Promise): Function to get tab info -
heartbeatInterval
(number, optional): Interval for sending heartbeats (default: 10000ms)
-
initialize(): Promise<void>
: Initialize the communicator -
getTabList(): void
: Request the current tab list -
requestAction<TResult>(targetTabId: string, action: string, payload?: any, onSuccess?: (result: TResult) => void, onError?: (error: string) => void): string | null
: Request an action from another tab -
cleanup(): void
: Clean up the communicator -
onEvent(subscriber: Subscriber<TabCommunicatorEvent>): () => void
: Subscribe to all events -
onTabListUpdated(subscriber: Subscriber<TabListItem[]>): () => void
: Subscribe to tab list updates -
getTabInfo(): TabInfo | null
: Get the current tab info -
isConnected(): boolean
: Check if the communicator is initialized
-
useRelay()
: Access the Relay instance -
useTabInfo()
: Get the current tab info -
useTabList()
: Get the list of connected tabs -
useRequestAction()
: Get a function to request actions from other tabs
The library includes a lightweight Observable implementation:
import { Observable } from "@powertoys/relay";
const counter$ = new Observable<number>();
// Subscribe
const unsubscribe = counter$.subscribe({
next: value => console.log("Count:", value),
error: err => console.error("Error:", err),
complete: () => console.log("Completed"),
});
// Emit values
counter$.next(1);
counter$.next(2);
// Unsubscribe
unsubscribe();
Define type-safe action handlers with proper payload and result types:
import { ActionHandler } from "@powertoys/relay";
interface UserPayload {
userId: string;
}
interface UserProfile {
id: string;
name: string;
email: string;
}
const getUserProfile: ActionHandler<UserPayload, UserProfile> = async payload => {
const { userId } = payload;
// Implementation...
return {
id: userId,
name: "John Doe",
email: "john@example.com",
};
};
This library uses SharedWorker which is supported in:
- Chrome 4+
- Firefox 29+
- Edge 79+
- Safari 10.1+
- Opera 10.6+
For browsers that don't support SharedWorker, you can implement a fallback using localStorage or BroadcastChannel.
MIT