Browser Integration

Lix is designed to integrate seamlessly with modern web browsers, providing a powerful change control system that runs entirely client-side. This guide covers the key aspects of integrating Lix into browser-based applications.

Storage Options

Lix supports multiple storage options in the browser:

In-Memory Storage

The simplest approach is to use in-memory storage, which is useful for temporary sessions or testing:

// Create and open a Lix instance in memory
const lixFile = await newLixFile();
const lix = await openLix({
  blob: lixFile,
  providePlugins: [jsonPlugin],
});

File System Access API

For persistent storage with user-controlled files, you can use the File System Access API:

// Get a file handle using the File System Access API
const fileHandle = await window.showSaveFilePicker({
  suggestedName: 'document.lix',
  types: [
    {
      description: 'Lix Files',
      accept: {
        'application/octet-stream': ['.lix'],
      },
    },
  ],
});

// Create a new Lix file
const lixFile = await newLixFile();

// Write the initial file
const writable = await fileHandle.createWritable();
await writable.write(lixFile);
await writable.close();

// Open the file
const file = await fileHandle.getFile();
const lix = await openLix({
  fileHandle,
  providePlugins: [jsonPlugin],
});

IndexedDB

For applications that need persistent storage without requiring the user to manage files:

// Using a helper library like idb-keyval
import { set, get } from 'idb-keyval';

// Save Lix file to IndexedDB
async function saveLixToIDB(lixFile) {
  await set('my-document.lix', lixFile);
}

// Load Lix file from IndexedDB
async function loadLixFromIDB() {
  const lixFile = await get('my-document.lix');
  if (lixFile) {
    return await openLix({
      blob: lixFile,
      providePlugins: [jsonPlugin],
    });
  }
  // Create new file if not found
  const newLixFile = await newLixFile();
  await set('my-document.lix', newLixFile);
  return await openLix({
    blob: newLixFile,
    providePlugins: [jsonPlugin],
  });
}

OPFS (Origin Private File System)

For Chrome and Chromium-based browsers, the Origin Private File System provides a powerful storage option:

// Check if OPFS is supported
if ('storage' in navigator && 'getDirectory' in navigator.storage) {
  const root = await navigator.storage.getDirectory();
  const fileHandle = await root.getFileHandle('document.lix', { create: true });
  
  // Create or load Lix file
  let lixFile;
  try {
    const file = await fileHandle.getFile();
    lixFile = await file.arrayBuffer();
  } catch (e) {
    // Create new file if reading fails
    lixFile = await newLixFile();
    const writable = await fileHandle.createWritable();
    await writable.write(lixFile);
    await writable.close();
  }
  
  // Open Lix with the file
  const lix = await openLix({
    blob: lixFile,
    providePlugins: [jsonPlugin],
  });
}

Web Workers

For improved performance, you can run Lix operations in a Web Worker to avoid blocking the main thread:

Main Thread

// Create a worker
const worker = new Worker('lix-worker.js');

// Send commands to the worker
worker.postMessage({
  type: 'CREATE_FILE',
});

// Listen for responses
worker.onmessage = (event) => {
  if (event.data.type === 'FILE_CREATED') {
    console.log('Lix file created with ID:', event.data.fileId);
  }
};

Worker Thread (lix-worker.js)

// Import Lix in the worker
importScripts('path-to-lix-bundle.js');

// Initialize Lix instance
let lix;

// Handle messages from the main thread
self.onmessage = async (event) => {
  const { type, data } = event.data;
  
  switch (type) {
    case 'CREATE_FILE':
      const lixFile = await newLixFile();
      lix = await openLix({
        blob: lixFile,
        providePlugins: [jsonPlugin],
      });
      self.postMessage({ type: 'FILE_CREATED', fileId: lix.id });
      break;
      
    case 'INSERT_FILE':
      const file = await lix.db
        .insertInto('file')
        .values(data)
        .returningAll()
        .executeTakeFirstOrThrow();
      self.postMessage({ type: 'FILE_INSERTED', file });
      break;
      
    // Add more command handlers
  }
};

Auto-saving

Implementing auto-save functionality with Lix:

async function setupAutoSave(lix, fileHandle, interval = 5000) {
  let lastSaveTime = Date.now();
  let pendingSave = false;
  
  // Function to save changes
  async function saveChanges() {
    if (pendingSave) return;
    
    pendingSave = true;
    try {
      const serialized = await lix.serialize();
      const writable = await fileHandle.createWritable();
      await writable.write(serialized);
      await writable.close();
      lastSaveTime = Date.now();
      console.log('Auto-saved at', new Date().toLocaleTimeString());
    } catch (err) {
      console.error('Auto-save failed:', err);
    } finally {
      pendingSave = false;
    }
  }
  
  // Set up periodic saving
  const intervalId = setInterval(async () => {
    // Only save if there are unsaved changes
    const hasChanges = await lix.hasUnsavedChanges();
    if (hasChanges) {
      await saveChanges();
    }
  }, interval);
  
  // Add event listener for page unload
  window.addEventListener('beforeunload', async (event) => {
    const hasChanges = await lix.hasUnsavedChanges();
    if (hasChanges) {
      // Synchronous save before unload
      await saveChanges();
    }
  });
  
  // Return cleanup function
  return () => clearInterval(intervalId);
}

Browser Compatibility

Lix is compatible with modern browsers that support WebAssembly and other required APIs:

BrowserMinimum VersionNotes
Chrome91+Full support including OPFS
Firefox90+No OPFS support
Safari15.4+Limited OPFS support
Edge91+Full support including OPFS

Feature Detection

Always use feature detection to ensure compatibility:

// Check for File System Access API
const hasFileSystemAccess = 'showOpenFilePicker' in window;

// Check for OPFS
const hasOPFS = 'storage' in navigator && 'getDirectory' in navigator.storage;

// Check for WebAssembly
const hasWasm = typeof WebAssembly === 'object' && 
               typeof WebAssembly.instantiate === 'function';

// Choose appropriate storage strategy based on available features
function chooseStorageStrategy() {
  if (hasFileSystemAccess) {
    return 'file-system-access';
  } else if (hasOPFS) {
    return 'opfs';
  } else {
    return 'indexeddb';
  }
}

Performance Considerations

Batching Operations

For better performance, batch related operations:

// Batch multiple changes in a single transaction
await lix.db.transaction().execute(async (trx) => {
  const snapshot = await trx
    .insertInto('snapshot')
    .values({ created_at: new Date().toISOString() })
    .returningAll()
    .executeTakeFirstOrThrow();
    
  // Insert multiple changes at once
  await trx
    .insertInto('change')
    .values(multipleChanges.map(change => ({
      ...change,
      snapshot_id: snapshot.id
    })))
    .execute();
});

Memory Usage

Monitor memory usage when working with large datasets:

// Check for performance.memory (Chrome only)
if (performance.memory) {
  console.log('JS Heap Size:', performance.memory.usedJSHeapSize / 1048576, 'MB');
}

// Use FinalizationRegistry to track object cleanup
const registry = new FinalizationRegistry((name) => {
  console.log(`${name} has been garbage collected`);
});

// Register objects for cleanup tracking
registry.register(largeDataObject, 'Large data object');

Network Considerations

Offline Support

Lix works well in offline scenarios, but you should handle synchronization when connectivity is restored:

// Listen for online/offline events
window.addEventListener('online', async () => {
  console.log('Connection restored, syncing changes...');
  await syncChangesToServer(lix);
});

window.addEventListener('offline', () => {
  console.log('Connection lost, working in offline mode');
});

// Check initial state
if (navigator.onLine) {
  console.log('Starting in online mode');
} else {
  console.log('Starting in offline mode');
}

Security Considerations

Lix runs entirely client-side, which means sensitive data doesn't need to leave the user's browser. However, consider the following security aspects:

  • Use HTTPS to protect data in transit when synchronizing
  • Implement proper authentication for server synchronization
  • Consider using the Web Crypto API for additional encryption if needed
  • Be cautious about which third-party scripts can access your application's data

Further Reading