Cross-Tab Synchronization Guide
Guide for implementing real-time storage synchronization across browser tabs and windows.
Overview
Strata Storage provides automatic synchronization of storage changes across multiple tabs/windows using the BroadcastChannel API and storage events.
Quick Start
import { Strata } from 'strata-storage';
// Enable sync
const storage = new Strata({
sync: {
enabled: true
}
});
// Listen for changes from other tabs
storage.subscribe((change) => {
if (change.source === 'remote') {
console.log('Another tab updated:', change.key);
}
});
Configuration
interface SyncConfig {
enabled?: boolean; // Enable sync
storages?: StorageType[]; // Which storages to sync
interval?: number; // Background sync interval (ms)
conflictResolution?: 'latest' | 'merge' | ((conflicts: unknown[]) => unknown);
}
Sync Mechanisms
BroadcastChannel (Primary)
// Modern browsers support BroadcastChannel
const storage = new Strata({
sync: {
enabled: true
}
});
// Changes are broadcast to all tabs
await storage.set('shared_data', value);
// All tabs receive update notification
Storage Events (Fallback)
// Fallback for older browsers
window.addEventListener('storage', (e) => {
if (e.key?.startsWith('strata_')) {
// Handle storage change
}
});
The storage-event fallback is localStorage-only (cross-tab); it does not claim
sessionStoragecross-tab support.
Message safety (v2.7.0)
Cross-tab sync is hardened against malformed/cross-origin noise on the channel:
- Each instance generates its origin ID with
crypto.randomUUID(), so a tab reliably ignores its own echoed messages. - Inbound
BroadcastChannelmessages are validated (shape-checked) before they're applied, so a malformed or unexpected payload can't corrupt local state.
Usage Examples
Real-Time Updates
class RealtimeSync {
constructor() {
this.storage = new Strata({
sync: { enabled: true }
});
// Subscribe to all changes
this.storage.subscribe(this.handleChange.bind(this));
}
handleChange(change: StorageChange) {
if (change.source === 'remote') {
// Update UI based on change
this.updateUI(change.key, change.newValue);
}
}
async updateData(key: string, value: unknown) {
// This will sync to all tabs
await this.storage.set(key, value);
}
}
User Authentication Sync
class AuthSync {
constructor() {
this.storage = new Strata({
sync: {
enabled: true,
storages: ['localStorage', 'sessionStorage']
}
});
// Listen for auth changes
this.storage.subscribe((change) => {
if (change.key === 'auth_token' && change.source === 'remote') {
if (change.newValue) {
this.onLogin(change.newValue);
} else {
this.onLogout();
}
}
});
}
async login(token: string) {
await this.storage.set('auth_token', token);
// All tabs are now logged in
}
async logout() {
await this.storage.remove('auth_token');
// All tabs are now logged out
}
}
Shopping Cart Sync
class CartSync {
async addItem(item: CartItem) {
const cart = await this.getCart();
cart.items.push(item);
// Update syncs to all tabs
await storage.set('shopping_cart', cart, {
storage: 'localStorage'
});
}
setupSync() {
storage.subscribe((change) => {
if (change.key === 'shopping_cart' && change.source === 'remote') {
this.renderCart(change.newValue);
}
});
}
}
Conflict Resolution
Latest Wins (Default)
const storage = new Strata({
sync: {
conflictResolution: 'latest'
}
});
Custom Merge
const storage = new Strata({
sync: {
conflictResolution: (conflicts) => {
// Custom merge logic
return conflicts.reduce((merged, change) => {
// Merge changes
return { ...merged, ...change.value };
}, {});
}
}
});
Version-Based
class VersionedSync {
async set(key: string, value: unknown) {
const versioned = {
value,
version: Date.now(),
tabId: this.tabId
};
await storage.set(key, versioned);
}
handleConflict(local: any, remote: any) {
// Use highest version
return local.version > remote.version ? local : remote;
}
}
Performance Optimization
Selective Sync
// Only sync specific storages
const storage = new Strata({
sync: {
enabled: true,
storages: ['localStorage'] // Don't sync memory/session
}
});
// Control per operation
await storage.set('local_only', value, {
syncEnabled: false // Don't sync this change
});
Browser Compatibility
| Feature | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
| BroadcastChannel | ✅ 54+ | ✅ 38+ | ❌ | ✅ 79+ |
| Storage Events | ✅ All | ✅ All | ✅ All | ✅ All |
Safari Workaround
// Safari doesn't support BroadcastChannel
// Falls back to localStorage events
const storage = new Strata({
sync: {
enabled: true,
// Automatically uses storage events on Safari
}
});
Common Patterns
Leader Election
class TabLeader {
private isLeader = false;
async electLeader() {
const leaderId = await storage.get('leader_id');
if (!leaderId || !(await this.isLeaderAlive(leaderId))) {
// Become leader
this.isLeader = true;
await storage.set('leader_id', this.tabId, {
ttl: 5000 // 5 second lease
});
// Renew lease
this.renewInterval = setInterval(() => {
storage.extendTTL('leader_id', 5000);
}, 4000);
}
}
}
State Synchronization
class AppStateSync {
private state: AppState = {};
constructor() {
// Load initial state
this.loadState();
// Sync state changes
storage.subscribe((change) => {
if (change.key === 'app_state' && change.source === 'remote') {
this.state = change.newValue;
this.render();
}
});
}
async updateState(updates: Partial<AppState>) {
this.state = { ...this.state, ...updates };
await storage.set('app_state', this.state);
}
}
Debugging
Enable Sync Logging
const storage = new Strata({
sync: { enabled: true },
debug: true,
onLog: (level, message, data) => {
if (message.includes('sync')) {
console.log('[Sync]', message, data);
}
}
});
Monitor Sync Events
// Log all sync activity
storage.subscribe((change) => {
console.table({
key: change.key,
source: change.source,
storage: change.storage,
timestamp: new Date(change.timestamp)
});
});
Best Practices
- Selective Sync: Only sync necessary data
- Handle Offline: Gracefully handle when sync is unavailable
- Version Control: Implement versioning for complex data
- Clean Up: Remove sync listeners when done