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:
Browser | Minimum Version | Notes |
---|
Chrome | 91+ | Full support including OPFS |
Firefox | 90+ | No OPFS support |
Safari | 15.4+ | Limited OPFS support |
Edge | 91+ | 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