中文 | English
Framework-agnostic browser cross-tab data bus.
By default each tab holds its own Dedicated Worker; when configured with workerMode: 'shared' or 'auto', same-origin tabs can reuse a single SharedWorker. In auto mode it degrades automatically through SharedWorker → Dedicated Worker → main-thread WebSocket. Same-origin tabs form a logical Worker cluster over BroadcastChannel; the SDK automatically coordinates sticky Topic owners, subscription reuse, new-Topic load distribution, failover, and page lifecycle, so the application only needs to subscribe to Topics and process data.
- Subscribable immediately after creation; subscriptions auto-queue while the connection is pending
- Subscriptions within the same tab are deduplicated by handler reference counting
- Topic owners are reused across same-origin tabs, reducing duplicate real-time subscriptions
- In SharedWorker mode, same-origin tabs reuse a single SharedWorker; each tab's port maintains its own independent connection, so refreshing or stopping one tab does not affect others
workerModesupportsdedicated/shared/auto;autodegrades via SharedWorker → Dedicated Worker → main-thread WebSocket, while explicitdedicateddegrades via Dedicated Worker → SharedWorker → main-thread WebSocket- With
transferable: true, ArrayBuffer messages are transmitted via Transferable while the object-message API stays unchanged - localStorage coordination writes are merged and flushed in batches; heartbeat and route confirmation use exponential backoff
- Existing Topic owners remain stable while alive; visibility changes do not move established subscriptions
- New Topics are assigned to the least-loaded eligible Worker
- Automatic owner recovery after an ungraceful exit: a lost handoff ACK or a crashed owner recovers through a TTL-gated re-election (bounded by
heartbeatIntervalMs + workerTtlMs), and each route acknowledgment / migration / recovery is observable as a boundedreliabilitytrace event - Opt-in adaptive owner weighting (
loadWeighting): traffic message/byte rates and heartbeat scheduling-lag steer NEW routes toward quieter, healthier Workers; existing routes stay sticky and the default remains pure topic-count routing (configuration) - Async credential refresh bridge for the Centrifuge worker (
credentialProvider): the Worker requests each freshgetToken/getChannelTokenfrom the main thread over a TOKEN_REQUEST/RESPONSE exchange, keeping function-valued options out of the structured-clone boundary - Wildcard subscriptions:
chat.*and*patterns match concrete topics at dispatch - Transport-neutral publication metadata (
messageId,timestamp) with canonical WebSocket/Centrifuge envelopes and legacy frame compatibility - Optional durable replay retention (
replay.retentionMs) and deduplication outcome metrics in trace snapshots - Built-in zero-dependency native WebSocket transport (
createWebSocketDataBus) for plain-WebSocket servers - Optional React hooks adapter (
cross-tab-worker-databus/hooks): StrictMode-safe bus lifecycle, auto-cleanup subscriptions - Optional Vue 3 composables adapter (
cross-tab-worker-databus/vue): lifecycle-safe bus, subscription, and status composables pagehidereleases resources automatically;pageshowrebuilds the Worker and connection automatically- Transport reconnect automatically restores the current owner's Topics
- Optional durable replay persistence with
appendBatchbulk writes (IndexedDB transaction coalescing; prune strategiescount/age/both) - Transport-level batch publishing:
publishBatchpacks bursts into one wire frame where the backend supports it - Single-object health verdict via
getHealthSummary()with a unified failure ledger and recovery context - Synchronous diagnostics:
getMetrics()snapshots the current trace window (throughput, dispatch latency percentiles, dedup outcomes),getDiagnostics().metrics/traceride in the diagnostics object, andgetDiagnostics().replayreports the buffered replay footprint (messages+ approximatebytes) - Opt-in coordination fallback over localStorage storage events when BroadcastChannel is unavailable
- After a tab exits abnormally, automatic migration happens via heartbeat TTL
- Automatically degrades to local mode when BroadcastChannel or localStorage is unavailable
- The persistence layer does not store connection addresses, raw Topic text, or message content
See the Capabilities Matrix for the full list of implemented, unimplemented, and planned capabilities.
pnpm add cross-tab-worker-databusThe core package has zero runtime dependencies. The Centrifuge transport
(cross-tab-worker-databus/centrifuge) declares centrifuge as an optional
peer dependency — install it only when you use the built-in Centrifuge backend:
pnpm add cross-tab-worker-databus centrifugeTabs that only use the local BroadcastChannel data bus (no WebSocket server)
do not need to install centrifuge at all.
import { createCentrifugeDataBus } from 'cross-tab-worker-databus/centrifuge';
interface ResourceEvent {
id: string;
version: number;
content: unknown;
}
const bus = createCentrifugeDataBus<ResourceEvent>({
connection: {
url: getConnectionUrl(),
options: getConnectionOptions()
}
});
const unsubscribe = bus.subscribe('resource.changed', ({ data }) => {
applyResourceEvent(data);
});
await bus.ready();
unsubscribe();
await bus.stop();The application does not need to handle Tab owner, Worker migration, page recovery, or re-subscription after reconnection.
The repository includes runnable multi-tab demo pages that showcase real-time data flow between publishing, receiving, cluster routing, Worker sessions, and the server:
pnpm install
pnpm build
pnpm examplesThen open http://localhost:4173/examples/demo/ in multiple tabs at the same time to see cross-tab data flowing. The demo page uses the public Centrifugo demo endpoint wss://faye.centrifugal.dev/connection/websocket by default; the address, Worker mode, and Topics can all be modified in-page. You can also switch to "local broadcast" mode, which does not depend on an external server and demonstrates multi-tab coordination purely through BroadcastChannel.
The demo page includes data-flow animations, an event stream, receive/dispatch latency metrics, and cluster Worker routing status.
const bus = createCentrifugeDataBus<ResourceEvent>({
connection: { url: 'wss://example.com/ws', options: {} }
});
// Subscribe before connection — queued until ready
const unsub = bus.subscribe('events.created', ({ data }) => {
console.log('event received:', data);
});
// Monitor connection status
const unsubStatus = bus.onStatus(status => {
console.log('transport status:', status);
});
// Handle errors
const unsubError = bus.onError(error => {
console.error('transport error:', error);
});
await bus.ready();
// Later: clean up
unsub();
unsubStatus();
unsubError();
await bus.stop();import { createCentrifugeDataBus } from 'cross-tab-worker-databus/centrifuge';
const bus = createCentrifugeDataBus<ResourceEvent>({
connection: {
url: 'wss://example.com/connection/websocket',
options: {}
},
workerMode: 'auto' // SharedWorker → Dedicated Worker → local fallback
});
bus.subscribe('resource.changed', ({ data }) => {
// Data arrives from any tab's owner Worker
apply(data);
});const publisher = createCentrifugeDataBus<ResourceEvent>({
connection: { url: getUrl(), options: {} }
});
const receiver = createCentrifugeDataBus<ResourceEvent>({
connection: { url: getUrl(), options: {} }
});
receiver.subscribe('resource.updated', ({ data }) => {
console.log('got update:', data);
});
await publisher.ready();
publisher.publish('resource.updated', { id: 'abc', version: 2, content: { title: 'hello' } });const bus = createCentrifugeDataBus<ResourceEvent>({
connection: { url: getUrl(), options: {} },
trace: {
enabled: true,
sink: event => console.log('trace:', event.type, event)
}
});
// Snapshot
const snapshot = bus.getClusterSnapshot();
console.log(snapshot.workers, snapshot.routes, snapshot.assignedTopics);Why is my subscription not receiving messages from the other tab?
Cross-tab delivery for a topic only has one transport subscription (the owner). The owner fans out received publications to all tabs via BroadcastChannel EVENT messages, so if the receiving tab's browser disables BroadcastChannel or storage, it degrades to local-only mode. Check bus.getStatus() and getClusterSnapshot().coordinated.
Does every tab open its own WebSocket?
With the default dedicated mode, yes — each tab owns a connection through its own Worker. With shared (or auto in shared-capable browsers), same-origin tabs reuse one SharedWorker process while each tab's port keeps an independent session. Topic ownership is deduplicated across tabs either way, so popular topics are only subscribed once per cluster.
What happens when the owning tab crashes?
Ownership migrates. A graceful exit (pagehide) performs a strict handoff; an uncontrolled crash (killed tab, browser kill) is recovered via the heartbeat TTL — worst case heartbeatIntervalMs + workerTtlMs (≈13s with defaults).
Do I need centrifuge installed?
Only if you use the built-in Centrifuge backend (cross-tab-worker-databus/centrifuge). It is an optional peer dependency; the core package has zero runtime dependencies.
How do I migrate from 0.1.x to 0.2.x?
centrifuge moved from dependencies to an optional peerDependency. If you use the Centrifuge backend, add it to your own dependencies (pnpm add centrifuge@^5.5.3); no code changes are required. See the 0.2.0 changelog.
pnpm install
pnpm check # typecheck + unit tests + build
pnpm test:e2e # Playwright multi-tab browser tests (requires Google Chrome)
pnpm pack --pack-destination /tmp