Cross-Tab Sync
Synchronize VectorDB state across browser tabs
LocalMode provides cross-tab synchronization to keep VectorDB instances in sync across multiple browser tabs. This prevents data inconsistencies when users have your app open in multiple tabs.
Overview
Cross-tab sync uses two browser APIs:
- Web Locks API — Prevents concurrent writes from corrupting data
- BroadcastChannel API — Notifies other tabs when data changes
Both APIs have fallbacks for unsupported browsers. If unavailable, operations proceed without synchronization (safe for single-tab usage).
Quick Start
Create a Lock Manager
The lock manager ensures only one tab can write at a time.
import { getLockManager } from '@localmode/core';
const locks = getLockManager('my-database');Create a Broadcaster
The broadcaster notifies other tabs of changes.
import { createBroadcaster } from '@localmode/core';
const broadcaster = createBroadcaster('my-database');Use Locks for Write Operations
Wrap write operations in locks to prevent conflicts.
await locks.withWriteLock('documents', async () => {
await db.add({
id: 'doc-1',
vector: embedding,
metadata: { text: 'Hello world' },
});
// Notify other tabs
broadcaster.notifyDocumentAdded('default', 'doc-1');
});Subscribe to Changes
React to changes from other tabs.
broadcaster.on('document_added', (message) => {
console.log(`Document ${message.documentId} added in another tab`);
// Refresh your UI or invalidate cache
});Lock Manager API
getLockManager(dbName)
Creates or retrieves a lock manager for a database.
import { getLockManager } from '@localmode/core';
const locks = getLockManager('my-database');Lock Methods
Prop
Type
Lock Options
Prop
Type
Lock Examples
Multiple tabs can hold read locks simultaneously:
// Read lock - multiple tabs can read at once
const data = await locks.withReadLock('documents', async () => {
return await db.search(queryVector, { k: 10 });
});Write locks are exclusive—only one tab can hold the lock:
// Write lock - exclusive access
await locks.withWriteLock('documents', async () => {
await db.add({ id: 'doc-1', vector, metadata });
});Non-blocking lock attempt—useful for optional optimizations:
// Try to get lock, return null if unavailable
const result = await locks.tryLock('documents', async () => {
await db.add({ id: 'doc-1', vector, metadata });
return 'success';
});
if (result === null) {
console.log('Another tab is writing, try again later');
}Fail if lock isn't acquired within timeout:
try {
await locks.withLock(
'documents',
async () => {
await db.add({ id: 'doc-1', vector, metadata });
},
{ timeout: 5000 } // 5 second timeout
);
} catch (error) {
console.error('Lock timeout - another tab is holding the lock');
}Broadcaster API
createBroadcaster(dbName)
Creates a broadcaster for cross-tab communication.
import { createBroadcaster } from '@localmode/core';
const broadcaster = createBroadcaster('my-database');Notification Methods
Prop
Type
Event Types
Subscribe to specific event types:
type BroadcastMessageType =
| 'document_added'
| 'document_updated'
| 'document_deleted'
| 'documents_deleted'
| 'collection_cleared'
| 'database_cleared'
| 'index_updated'
| 'leader_elected'
| 'leader_ping';Subscription Methods
// Subscribe to specific event
const unsubscribe = broadcaster.on('document_added', (message) => {
console.log('Document added:', message.documentId);
});
// Subscribe to all events
const unsubscribeAll = broadcaster.onAny((message) => {
console.log('Event:', message.type);
});
// Clean up
unsubscribe();
unsubscribeAll();Message Structure
Prop
Type
Leader Election
For tasks that should only run in one tab (like background sync), use leader election:
const broadcaster = createBroadcaster('my-database');
// Try to become the leader
const isLeader = await broadcaster.electLeader();
if (isLeader) {
console.log('This tab is the leader');
// Start background sync, cleanup tasks, etc.
startBackgroundSync();
}
// Check leader status
if (broadcaster.getIsLeader()) {
// Run leader-only tasks
}
// Resign leadership (e.g., before tab closes)
broadcaster.resignLeadership();Leader election uses localStorage to coordinate between tabs. The leader sends periodic heartbeats—if a leader doesn't ping for 10 seconds, another tab can take over.
Full Integration Example
import { createVectorDB, getLockManager, createBroadcaster, embed } from '@localmode/core';
import { transformers } from '@localmode/transformers';
// Setup
const db = await createVectorDB({ name: 'documents', dimensions: 384 });
const locks = getLockManager('documents');
const broadcaster = createBroadcaster('documents');
const embeddingModel = transformers.embedding('Xenova/all-MiniLM-L6-v2');
// Subscribe to changes from other tabs
broadcaster.on('document_added', async ({ documentId }) => {
console.log(`Refresh UI - document ${documentId} added in another tab`);
// Optionally refresh your document list or clear caches
});
broadcaster.on('database_cleared', () => {
console.log('Database was cleared in another tab');
// Reset your UI state
});
// Add document with synchronization
async function addDocument(text: string) {
const { embedding } = await embed({
model: embeddingModel,
value: text,
});
const id = crypto.randomUUID();
await locks.withWriteLock('documents', async () => {
await db.add({
id,
vector: embedding,
metadata: { text, createdAt: Date.now() },
});
// Notify other tabs
broadcaster.notifyDocumentAdded('default', id);
});
return id;
}
// Clean up on page unload
window.addEventListener('beforeunload', () => {
broadcaster.close();
});Browser Compatibility
| Feature | Chrome | Edge | Firefox | Safari |
|---|---|---|---|---|
| Web Locks API | 69+ | 79+ | 96+ | 15.4+ |
| BroadcastChannel | 54+ | 79+ | 38+ | 15.4+ |
If these APIs are unavailable (e.g., in older browsers or certain WebView environments), operations proceed without synchronization. This is safe for single-tab usage but may cause issues with multiple tabs.
Feature Detection
import { LockManager, Broadcaster } from '@localmode/core';
if (LockManager.isSupported()) {
console.log('Web Locks API available');
}
if (Broadcaster.isSupported()) {
console.log('BroadcastChannel API available');
}