Lifo supports automatic persistence of the virtual filesystem to IndexedDB in browser environments. When enabled, filesystem changes are automatically saved and restored across page reloads.
Enabling Persistence
Persistence is enabled via the persist option when creating a sandbox:
import { Sandbox } from '@lifo-sh/core';
const sandbox = await Sandbox.create({
persist: true // Enable IndexedDB persistence
});
Persistence is disabled by default. You must explicitly set persist: true to enable it.
How It Works
Boot sequence
When persist: true is set:
- The persistence backend opens the IndexedDB database
- The kernel attempts to load a previously saved filesystem tree
- If found, the saved state is deserialized and loaded into the VFS
- If not found, a fresh filesystem is initialized
Runtime behavior
During sandbox operation:
- Filesystem changes trigger watch events in the VFS
- Changes are debounced (1000ms default) to batch writes
- The entire VFS tree is serialized and saved to IndexedDB
- Virtual directories (
/proc, /dev) are excluded from persistence
Restoration
On subsequent page loads:
- The saved filesystem tree is automatically loaded from IndexedDB
- Your files, directories, and permissions are restored
- Virtual providers are re-registered for
/proc and /dev
Serialization Details
The persistence system serializes the VFS tree to a compact binary format:
// Internal serialization (you don't call this directly)
const serialized = serialize(vfs.getRoot());
// Returns: SerializedNode with compressed structure
// Restoration
const restored = deserialize(serialized);
vfs.loadFromSerialized(restored);
What Gets Persisted
✅ Included:
- All files and their contents (text and binary)
- Directory structure
- File permissions (mode)
- Timestamps (ctime, mtime)
❌ Excluded:
/proc directory (virtual, regenerated at boot)
/dev directory (virtual, regenerated at boot)
- Running processes and shell state
- Port registry and virtual network handlers
Testing Persistence
Here’s how to test that persistence works:
import { Sandbox } from '@lifo-sh/core';
// First session: create and save data
async function sessionOne() {
const sandbox = await Sandbox.create({ persist: true });
// Create some files
await sandbox.fs.writeFile('/home/user/note.txt', 'Remember this!');
await sandbox.fs.mkdir('/home/user/projects');
await sandbox.fs.writeFile('/home/user/projects/app.js', 'console.log("hello");');
console.log('Files created. Refresh the page to test persistence.');
// Changes are auto-saved (debounced)
// Wait a bit to ensure save completes
await new Promise(resolve => setTimeout(resolve, 1500));
}
// Second session: verify data persisted
async function sessionTwo() {
const sandbox = await Sandbox.create({ persist: true });
// Check if files exist
const noteExists = await sandbox.fs.exists('/home/user/note.txt');
console.log('note.txt exists:', noteExists); // true
// Read content
const note = await sandbox.fs.readFile('/home/user/note.txt');
console.log('Content:', note); // "Remember this!"
// Verify directory structure
const entries = await sandbox.fs.readdir('/home/user');
console.log('Files:', entries.map(e => e.name)); // ["note.txt", "projects", ...]
}
// Call sessionOne first, then refresh and call sessionTwo
Persistence Backend
By default, Lifo uses IndexedDBPersistenceBackend for browser environments:
import { IndexedDBPersistenceBackend } from '@lifo-sh/core';
// This is used internally by default
const backend = new IndexedDBPersistenceBackend();
await backend.open();
// Load saved tree
const data = await backend.loadTree();
// Save tree
await backend.saveTree(serialized);
Custom Backend
You can implement a custom persistence backend by implementing the PersistenceBackend interface:
import { Kernel, PersistenceBackend } from '@lifo-sh/core';
import type { SerializedNode } from '@lifo-sh/core';
class LocalStorageBackend implements PersistenceBackend {
private key = 'lifo-vfs-tree';
async open(): Promise<void> {
// No-op for localStorage
}
async loadTree(): Promise<SerializedNode | null> {
const data = localStorage.getItem(this.key);
return data ? JSON.parse(data) : null;
}
async saveTree(tree: SerializedNode): Promise<void> {
localStorage.setItem(this.key, JSON.stringify(tree));
}
}
// Use custom backend
const kernel = new Kernel(new LocalStorageBackend());
await kernel.boot({ persist: true });
Filesystem changes are debounced to avoid excessive writes:
// Internal implementation (from PersistenceManager.ts)
const DEBOUNCE_MS = 1000; // 1 second
// Each filesystem change schedules a save
vfs.watch(() => {
persistence.scheduleSave(vfs.getRoot());
});
// scheduleSave debounces writes
scheduleSave(root: INode): void {
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(() => {
this.save(root).catch(() => {});
this.timer = null;
}, DEBOUNCE_MS);
}
This means:
- Rapid file changes trigger only one save after 1 second of inactivity
- Performance remains good even with many file operations
- Changes are saved automatically without manual intervention
Snapshots vs Persistence
Lifo offers two ways to save filesystem state:
Persistence (Automatic)
const sandbox = await Sandbox.create({ persist: true });
// Changes are auto-saved to IndexedDB
// Restored automatically on next load
Use when:
- You want automatic, transparent state management
- Building a browser-based IDE or terminal
- Users expect their work to persist across sessions
Snapshots (Manual)
// Export entire VFS as tar.gz
const snapshot = await sandbox.fs.exportSnapshot();
// Save to file, send to server, etc.
const blob = new Blob([snapshot], { type: 'application/gzip' });
// Later: restore from snapshot
const sandbox2 = await Sandbox.create();
await sandbox2.fs.importSnapshot(snapshot);
Use when:
- You need portable, explicit save/load functionality
- Implementing save/load buttons in your UI
- Sharing filesystem state between users or devices
- Creating checkpoints or versioned snapshots
Complete Example
Here’s a complete example demonstrating persistence in a browser application:
import { Sandbox } from '@lifo-sh/core';
class PersistentTerminal {
private sandbox: Sandbox | null = null;
async init() {
// Create sandbox with persistence
this.sandbox = await Sandbox.create({
persist: true,
cwd: '/home/user'
});
// Check if this is first run
const welcomeExists = await this.sandbox.fs.exists('/home/user/.welcome');
if (!welcomeExists) {
// First run: create welcome file
await this.sandbox.fs.writeFile(
'/home/user/.welcome',
'Welcome to Lifo! This file persists across reloads.\n'
);
await this.sandbox.commands.run('cat .welcome');
} else {
console.log('Welcome back! Your files are restored.');
}
// Run some commands
await this.executeCommand('ls -la');
}
async executeCommand(cmd: string) {
if (!this.sandbox) return;
const result = await this.sandbox.commands.run(cmd, {
onStdout: (data) => console.log(data),
onStderr: (data) => console.error(data)
});
return result;
}
async saveAndExit() {
// Ensure final save completes
if (this.sandbox) {
// Wait for debounce to complete
await new Promise(resolve => setTimeout(resolve, 1500));
this.sandbox.destroy();
}
}
}
// Usage
const terminal = new PersistentTerminal();
await terminal.init();
// User creates files
await terminal.executeCommand('echo "my data" > data.txt');
await terminal.executeCommand('mkdir projects');
// On page unload, ensure save completes
window.addEventListener('beforeunload', () => {
terminal.saveAndExit();
});
Troubleshooting
Data not persisting
- Verify
persist: true is set
- Ensure browser supports IndexedDB
- Check browser storage quota isn’t exceeded
- Wait for debounce period (1 second) after changes
Storage quota
// Check IndexedDB quota (browser API)
if ('storage' in navigator && 'estimate' in navigator.storage) {
const estimate = await navigator.storage.estimate();
console.log('Used:', estimate.usage, 'bytes');
console.log('Quota:', estimate.quota, 'bytes');
console.log('Percentage:', (estimate.usage / estimate.quota * 100).toFixed(2) + '%');
}
Clear persisted data
// In browser console or application code
indexedDB.deleteDatabase('lifo-vfs');
location.reload();
API Reference
See the Persistence API reference and Kernel API reference for details on the persistence system internals.