LocalMode
Core

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

FeatureChromeEdgeFirefoxSafari
Web Locks API69+79+96+15.4+
BroadcastChannel54+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');
}

On this page