Web Locks API, BroadcastChannel & Multi-Tab Sync Studio
Architect robust multi-tab web applications. Simulate navigator.locks shared and exclusive locks, test instant failover leader elections, broadcast reactive state changes via BroadcastChannel, and observe guaranteed deadlock-free tab crash recoveries.
1. Simulated Browser Tab Cluster (Same Origin)
2. Lock Resource Table (navigator.locks.query())
3. Production Multi-Tab Leader Election & Sync Pattern
// TypeScript: Resilient Multi-Tab Leader Election & Sync
const syncBus = new BroadcastChannel('app_state_channel');
async function electLeader(onElected: () => void) {
if (!('locks' in navigator)) {
console.warn('Web Locks API not supported; falling back to single-tab mode');
return;
}
// Request exclusive lock on 'singleton_leader'
await navigator.locks.request('singleton_leader', async (lock) => {
console.log('[Leader] Lock acquired. This tab is now the active leader.');
syncBus.postMessage({ type: 'LEADER_ELECTED', tabId: window.name });
onElected();
// Hold the lock indefinitely until this tab closes or crashes
await new Promise(() => {});
});
}
// Transactional Mutex across tabs
async function executeAtomicDbMutation<T>(mutationFn: () => Promise<T>): Promise<T> {
return await navigator.locks.request('idb_write_mutex', { mode: 'exclusive' }, async () => {
const result = await mutationFn();
syncBus.postMessage({ type: 'DATA_UPDATED', timestamp: Date.now() });
return result;
});
}
⚠️ 5 Fatal Traps in Web Locks & Multi-Tab Architectures
1. Permanent Lock Holding via Unresolving Promises in Mutexes
The Web Lock is released ONLY when the callback promise resolves or rejects. If an asynchronous database operation hangs (e.g. awaiting an unfulfilled network response without an AbortSignal timeout), the lock remains frozen forever, permanently starving all other browser tabs from writing to IndexedDB.
2. Inadvertent Deadlocks through Nested Lock Acquisition
If Tab 1 acquires Lock A and requests Lock B, while Tab 2 acquires Lock B and requests Lock A, an unrecoverable classic deadlock occurs. Web Locks does NOT automatically detect deadlocks. Always acquire locks in a globally consistent alphabetical order or use lock stealing (steal: true).
3. BroadcastChannel Memory Leaks from Missing 'close()'
Creating new BroadcastChannel() inside short-lived Single Page Application (SPA) view components or Web Workers without invoking channel.close() upon component unmount retains the event listener in the browser engine, leaking memory and duplicating message handler executions.
4. Overlooking 'ifAvailable: true' for Non-Blocking Probes
When a background worker wants to perform opportunistic cache cleanup, queuing behind a heavy user transaction degrades responsiveness. Developers should pass { ifAvailable: true }. If the lock is currently held, the callback is invoked with null, allowing immediate non-blocking skip.
5. Cross-Origin iframes Without Permissions Delegation
Web Locks are strictly scoped to the origin (scheme + domain + port). However, if an embedded cross-origin iframe attempts to coordinate with parent or sibling frames, navigator.locks operates in an entirely isolated namespace and provides zero mutual exclusion against the host page.