@componentor/fs
v3.0.20
Published
High-performance OPFS-based Node.js fs polyfill with true sync API, VFS binary format, and bidirectional OPFS mirroring
Maintainers
Readme
@componentor/fs
High-performance OPFS-based Node.js fs polyfill for the browser
A virtual filesystem powered by a custom binary format (VFS), SharedArrayBuffer + Atomics for true synchronous APIs, multi-tab coordination via Web Locks, and bidirectional OPFS mirroring.
import { VFSFileSystem } from '@componentor/fs';
const fs = new VFSFileSystem();
// Sync API (requires crossOriginIsolated — blocks until ready on first call)
fs.writeFileSync('/hello.txt', 'Hello World!');
const data = fs.readFileSync('/hello.txt', 'utf8');
// Async API (always available)
await fs.promises.writeFile('/async.txt', 'Async data');
const content = await fs.promises.readFile('/async.txt', 'utf8');Features
- True Sync API —
readFileSync,writeFileSync, etc. via SharedArrayBuffer + Atomics - Async API —
promises.readFile,promises.writeFile— works without COOP/COEP - VFS Binary Format — All data in a single
.vfs.binfile for maximum throughput - OPFS Sync — Bidirectional mirror to real OPFS files (enabled by default)
- Multi-tab Safe — Leader/follower architecture with automatic failover via
navigator.locks - FileSystemObserver — External OPFS changes synced back to VFS automatically (Chrome 129+)
- isomorphic-git Ready — Full compatibility with git operations
- Zero Config — Workers inlined at build time, no external worker files needed
- TypeScript First — Complete type definitions included
Installation
npm install @componentor/fsQuick Start
import { VFSFileSystem } from '@componentor/fs';
const fs = new VFSFileSystem({ root: '/my-app' });
// Option 1: Sync API (blocks on first call until VFS is ready)
fs.mkdirSync('/my-app/src', { recursive: true });
fs.writeFileSync('/my-app/src/index.js', 'console.log("Hello!");');
const code = fs.readFileSync('/my-app/src/index.js', 'utf8');
// Option 2: Async init (non-blocking)
await fs.init(); // wait for VFS to be ready
const files = await fs.promises.readdir('/my-app/src');
const stats = await fs.promises.stat('/my-app/src/index.js');Convenience Helpers
import { createFS, getDefaultFS, init } from '@componentor/fs';
// Create with config
const fs = createFS({ root: '/repo', debug: true });
// Lazy singleton (created on first access)
const defaultFs = getDefaultFS();
// Async init helper
await init(); // initializes the default singletonConfiguration
const fs = new VFSFileSystem({
root: '/', // OPFS root directory (default: '/')
mode: 'hybrid', // 'hybrid' | 'vfs' | 'opfs' (default: 'hybrid')
opfsSyncRoot: undefined, // Custom OPFS root for mirroring (default: same as root)
uid: 0, // User ID for file ownership (default: 0)
gid: 0, // Group ID for file ownership (default: 0)
umask: 0o022, // File creation mask (default: 0o022)
strictPermissions: false, // Enforce Unix permissions (default: false)
sabSize: 4194304, // SharedArrayBuffer size in bytes (default: 4MB)
debug: false, // Enable debug logging (default: false)
swUrl: undefined, // URL of the service worker script (default: auto-resolved)
swScope: undefined, // Custom service worker scope (default: auto-scoped per root)
limits: { // Upper bounds for VFS validation (prevents corrupt data from causing OOM)
maxInodes: 4_000_000, // Max inode count (default: 4M)
maxBlocks: 4_000_000, // Max data blocks (default: 4M)
maxPathTable: 256 * 1024 * 1024, // Max path table bytes (default: 256MB)
maxVFSSize: 100 * 1024 * 1024 * 1024, // Max .vfs.bin size (default: 100GB)
maxPayload: 2 * 1024 * 1024 * 1024, // Max single SAB payload (default: 2GB)
},
});Filesystem Modes
The mode option controls how the filesystem stores data:
| Mode | Storage | OPFS Sync | Speed | Resilience |
|------|---------|-----------|-------|------------|
| hybrid (default) | VFS binary + OPFS mirror | Bidirectional | Fast | High |
| vfs | VFS binary only | None | Fastest | Medium |
| opfs | Real OPFS files only | N/A | Slower | Highest |
// Hybrid mode (default) — best of both worlds
const fs = new VFSFileSystem({ mode: 'hybrid' });
fs.writeFileSync('/file.txt', 'data');
// → stored in .vfs.bin AND mirrored to real OPFS files
// VFS-only mode — maximum performance, no OPFS mirroring
const fastFs = new VFSFileSystem({ mode: 'vfs' });
// OPFS-only mode — no VFS binary, operates directly on OPFS files
const safeFs = new VFSFileSystem({ mode: 'opfs' });Hybrid mode mirrors all VFS mutations to real OPFS files in the background:
- VFS → OPFS: Every write, delete, mkdir, rename is replicated after the sync operation completes (zero performance impact on the hot path)
- OPFS → VFS: A
FileSystemObserverwatches for external changes and syncs them back (Chrome 129+)
This allows external tools (browser DevTools, OPFS extensions) to see and modify files while VFS handles all the fast read/write operations internally.
Corruption Fallback
In hybrid mode, if VFS corruption is detected during initialization, the filesystem automatically falls back to opfs mode. The init() call rejects with an error describing the corruption, but all filesystem operations continue working via OPFS:
const fs = new VFSFileSystem(); // hybrid mode
try {
await fs.init();
} catch (err) {
// VFS was corrupt — system is now running in OPFS mode
console.warn(err.message); // "Falling back to OPFS mode: <reason>"
console.log(fs.mode); // 'opfs'
}
// Filesystem still works — reads/writes go through OPFS
fs.writeFileSync('/file.txt', 'still works!');Runtime Mode Switching
Use setMode() to switch modes at runtime. This is useful for IDE workflows where you want to recover from corruption:
// Corruption detected, currently in OPFS fallback mode
console.log(fs.mode); // 'opfs'
// Repair the VFS binary
await repairVFS('/my-app');
// Switch back to hybrid mode
await fs.setMode('hybrid');
console.log(fs.mode); // 'hybrid'setMode() terminates internal workers, allocates fresh shared memory, and reinitializes the filesystem in the requested mode.
Service Worker Setup (Multi-Tab)
Multi-tab coordination requires a service worker that acts as a MessagePort broker between tabs. The built service worker is shipped at dist/workers/service.worker.js. Unlike regular workers (which are resolved by the bundler), service workers must be served as a real file at a public URL.
Most bundlers (Vite, webpack) handle new URL('./workers/service.worker.js', import.meta.url) automatically, but if the default resolution doesn't work in your setup, use the swUrl option:
const fs = new VFSFileSystem({
swUrl: '/vfs-service-worker.js', // your public URL
});Vite example — copy the file to public/:
cp node_modules/@componentor/fs/dist/workers/service.worker.js public/vfs-service-worker.jsconst fs = new VFSFileSystem({ swUrl: '/vfs-service-worker.js' });If you only use a single tab, the service worker is not needed — the tab always runs as the leader.
COOP/COEP Headers
To enable the sync API, your page must be crossOriginIsolated. Add these headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpWithout these headers, only the async (promises) API is available.
Vite
// vite.config.ts
export default defineConfig({
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
});Express
app.use((req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin');
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp');
next();
});Vercel
{
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Cross-Origin-Opener-Policy", "value": "same-origin" },
{ "key": "Cross-Origin-Embedder-Policy", "value": "require-corp" }
]
}
]
}Runtime Check
if (crossOriginIsolated) {
// Sync + async APIs available
fs.writeFileSync('/fast.txt', 'blazing fast');
} else {
// Async API only
await fs.promises.writeFile('/fast.txt', 'still fast');
}Benchmarks
Tested against LightningFS (IndexedDB-based) in Chrome with crossOriginIsolated enabled:
| Operation | LightningFS | VFS Sync | VFS Promises | Winner | |-----------|------------|----------|-------------|--------| | Write 100 x 1KB | 46ms | 12ms | 23ms | VFS 4x | | Write 100 x 4KB | 36ms | 13ms | 22ms | VFS 2.8x | | Read 100 x 1KB | 19ms | 2ms | 14ms | VFS 9x | | Read 100 x 4KB | 62ms | 2ms | 13ms | VFS 28x | | Large 10 x 1MB | 11ms | 10ms | 17ms | VFS 1.1x | | Batch Write 500 x 256B | 138ms | 50ms | 75ms | VFS 2.8x | | Batch Read 500 x 256B | 73ms | 7ms | 91ms | VFS 10x |
Key takeaways:
- Reads are 9-28x faster — VFS binary format eliminates IndexedDB overhead
- Writes are 2.8-4x faster — Single binary file vs individual OPFS/IDB entries
- Batch operations are 2.8-10x faster — VFS excels at many small operations
- VFS Sync is the fastest path (SharedArrayBuffer + Atomics, zero async overhead)
Run benchmarks yourself:
npm run benchmark:openAPI Reference
Sync API (requires crossOriginIsolated)
// Read/Write
fs.readFileSync(path, options?): Uint8Array | string
fs.writeFileSync(path, data, options?): void
fs.appendFileSync(path, data): void
// Directories
fs.mkdirSync(path, options?): void
fs.rmdirSync(path, options?): void
fs.rmSync(path, options?): void
fs.readdirSync(path, options?): string[] | Dirent[]
// File Operations
fs.unlinkSync(path): void
fs.renameSync(oldPath, newPath): void
fs.copyFileSync(src, dest, mode?): void
fs.truncateSync(path, len?): void
fs.symlinkSync(target, path): void
fs.readlinkSync(path): string
fs.linkSync(existingPath, newPath): void
// Info
fs.statSync(path): Stats
fs.lstatSync(path): Stats
fs.existsSync(path): boolean
fs.accessSync(path, mode?): void
fs.realpathSync(path): string
// Metadata
fs.chmodSync(path, mode): void
fs.chownSync(path, uid, gid): void
fs.utimesSync(path, atime, mtime): void
// File Descriptors
fs.openSync(path, flags?, mode?): number
fs.closeSync(fd): void
fs.readSync(fd, buffer, offset?, length?, position?): number
fs.writeSync(fd, buffer, offset?, length?, position?): number
fs.fstatSync(fd): Stats
fs.ftruncateSync(fd, len?): void
fs.fdatasyncSync(fd): void
// Temp / Flush
fs.mkdtempSync(prefix): string
fs.flushSync(): voidAsync API (always available)
// Read/Write
fs.promises.readFile(path, options?): Promise<Uint8Array | string>
fs.promises.writeFile(path, data, options?): Promise<void>
fs.promises.appendFile(path, data): Promise<void>
// Directories
fs.promises.mkdir(path, options?): Promise<void>
fs.promises.rmdir(path, options?): Promise<void>
fs.promises.rm(path, options?): Promise<void>
fs.promises.readdir(path, options?): Promise<string[] | Dirent[]>
// File Operations
fs.promises.unlink(path): Promise<void>
fs.promises.rename(oldPath, newPath): Promise<void>
fs.promises.copyFile(src, dest, mode?): Promise<void>
fs.promises.truncate(path, len?): Promise<void>
fs.promises.symlink(target, path): Promise<void>
fs.promises.readlink(path): Promise<string>
fs.promises.link(existingPath, newPath): Promise<void>
// Info
fs.promises.stat(path): Promise<Stats>
fs.promises.lstat(path): Promise<Stats>
fs.promises.exists(path): Promise<boolean>
fs.promises.access(path, mode?): Promise<void>
fs.promises.realpath(path): Promise<string>
// Metadata
fs.promises.chmod(path, mode): Promise<void>
fs.promises.chown(path, uid, gid): Promise<void>
fs.promises.utimes(path, atime, mtime): Promise<void>
// Advanced
fs.promises.open(path, flags?, mode?): Promise<FileHandle>
fs.promises.opendir(path): Promise<Dir>
fs.promises.mkdtemp(prefix): Promise<string>
// Flush
fs.promises.flush(): Promise<void>Streams API
// Readable stream (Web Streams API)
const stream = fs.createReadStream('/large-file.bin', {
start: 0, // byte offset to start
end: 1024, // byte offset to stop
highWaterMark: 64 * 1024, // chunk size (default: 64KB)
});
for await (const chunk of stream) {
console.log('Read chunk:', chunk.length, 'bytes');
}
// Writable stream
const writable = fs.createWriteStream('/output.bin');
const writer = writable.getWriter();
await writer.write(new Uint8Array([1, 2, 3]));
await writer.close();Instance Methods
// Get the current filesystem mode
fs.mode: 'hybrid' | 'vfs' | 'opfs'
// Switch mode at runtime (terminates workers, reinitializes)
await fs.setMode('hybrid' | 'vfs' | 'opfs'): Promise<void>
// Non-blocking async init (waits for VFS to be ready)
await fs.init(): Promise<void>Watch API
// Watch for changes (supports recursive + AbortSignal)
const ac = new AbortController();
const watcher = fs.watch('/dir', { recursive: true, signal: ac.signal }, (eventType, filename) => {
console.log(eventType, filename); // 'rename' 'newfile.txt' or 'change' 'file.txt'
});
watcher.close(); // or ac.abort()
// Watch specific file with stat polling
fs.watchFile('/file.txt', { interval: 1000 }, (curr, prev) => {
console.log('File changed:', curr.mtimeMs !== prev.mtimeMs);
});
fs.unwatchFile('/file.txt');
// Async iterable (promises API)
for await (const event of fs.promises.watch('/dir', { recursive: true })) {
console.log(event.eventType, event.filename);
}Path Utilities
import { path } from '@componentor/fs';
path.join('/foo', 'bar', 'baz') // '/foo/bar/baz'
path.resolve('foo', 'bar') // '/foo/bar'
path.dirname('/foo/bar/baz.txt') // '/foo/bar'
path.basename('/foo/bar/baz.txt') // 'baz.txt'
path.extname('/foo/bar/baz.txt') // '.txt'
path.normalize('/foo//bar/../baz') // '/foo/baz'
path.isAbsolute('/foo') // true
path.relative('/foo/bar', '/foo/baz') // '../baz'
path.parse('/foo/bar/baz.txt') // { root, dir, base, ext, name }
path.format({ dir: '/foo', name: 'bar', ext: '.txt' }) // '/foo/bar.txt'Constants
import { constants } from '@componentor/fs';
constants.F_OK // 0 - File exists
constants.R_OK // 4 - File is readable
constants.W_OK // 2 - File is writable
constants.X_OK // 1 - File is executable
constants.COPYFILE_EXCL // 1 - Fail if dest exists
constants.O_RDONLY // 0
constants.O_WRONLY // 1
constants.O_RDWR // 2
constants.O_CREAT // 64
constants.O_EXCL // 128
constants.O_TRUNC // 512
constants.O_APPEND // 1024Maintenance Helpers
Standalone utilities for VFS maintenance, recovery, and migration. Must be called from a Worker context (sync access handle requirement). Close any running VFSFileSystem instance first.
import { unpackToOPFS, loadFromOPFS, repairVFS } from '@componentor/fs';
// Export VFS contents to real OPFS files (clears existing OPFS files first)
const { files, directories } = await unpackToOPFS('/my-app');
// Rebuild VFS from real OPFS files (deletes .vfs.bin, creates fresh VFS)
const { files, directories } = await loadFromOPFS('/my-app');
// Attempt to recover files from a corrupt VFS binary
const { recovered, lost, entries } = await repairVFS('/my-app');
console.log(`Recovered ${recovered} entries, lost ${lost}`);
for (const entry of entries) {
console.log(` ${entry.type} ${entry.path} (${entry.size} bytes)`);
}| Function | Description |
|----------|-------------|
| unpackToOPFS(root?) | Read all files from VFS, write to real OPFS paths |
| loadFromOPFS(root?) | Read all OPFS files, create fresh VFS with their contents |
| repairVFS(root?) | Scan corrupt .vfs.bin for recoverable inodes, rebuild fresh VFS |
isomorphic-git Integration
import { VFSFileSystem } from '@componentor/fs';
import git from 'isomorphic-git';
import http from 'isomorphic-git/http/web';
const fs = new VFSFileSystem({ root: '/repo' });
// Clone a repository
await git.clone({
fs,
http,
dir: '/repo',
url: 'https://github.com/user/repo',
corsProxy: 'https://cors.isomorphic-git.org',
});
// Check status
const status = await git.statusMatrix({ fs, dir: '/repo' });
// Stage and commit
await git.add({ fs, dir: '/repo', filepath: '.' });
await git.commit({
fs,
dir: '/repo',
message: 'Initial commit',
author: { name: 'User', email: '[email protected]' },
});Architecture
┌──────────────────────────────────────────────────────────────────┐
│ Main Thread │
│ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ Sync API │ │ Async API │ │ Path / Constants │ │
│ │ readFileSync │ │ promises. │ │ join, dirname, etc. │ │
│ │writeFileSync │ │ readFile │ └────────────────────────┘ │
│ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │
│ SAB + Atomics postMessage │
└─────────┼─────────────────┼──────────────────────────────────────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────────┐
│ sync-relay Worker (Leader) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ VFS Engine │ │
│ │ ┌──────────────────┐ ┌─────────────┐ ┌──────────────┐ │ │
│ │ │ VFS Binary File │ │ Inode/Path │ │ Block Data │ │ │
│ │ │ (.vfs.bin OPFS) │ │ Table │ │ Region │ │ │
│ │ └──────────────────┘ └─────────────┘ └──────────────┘ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │ │
│ notifyOPFSSync() │
│ (fire & forget) │
└────────────────────────────┼─────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ opfs-sync Worker │
│ ┌────────────────────┐ ┌────────────────────────────────────┐ │
│ │ VFS → OPFS Mirror │ │ FileSystemObserver (OPFS → VFS) │ │
│ │ (queue + echo │ │ External changes detected and │ │
│ │ suppression) │ │ synced back to VFS engine │ │
│ └────────────────────┘ └────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
Multi-tab (via Service Worker + navigator.locks):
Tab 1 (Leader) ←→ Service Worker ←→ Tab 2 (Follower)
Tab 1 holds VFS engine, Tab 2 forwards requests via MessagePort
If Tab 1 dies, Tab 2 auto-promotes to leaderBrowser Support
| Browser | Sync API | Async API | |---------|----------|-----------| | Chrome 102+ | Yes | Yes | | Edge 102+ | Yes | Yes | | Firefox 111+ | Yes* | Yes | | Safari 15.2+ | No** | Yes | | Opera 88+ | Yes | Yes |
* Firefox requires dom.workers.modules.enabled flag
** Safari doesn't support SharedArrayBuffer in the required context
Troubleshooting
"SharedArrayBuffer is not defined"
Your page is not crossOriginIsolated. Add COOP/COEP headers (see above). The async API still works without them.
"Sync API requires crossOriginIsolated"
Same issue — sync methods (readFileSync, etc.) need SharedArrayBuffer. Use fs.promises.* as a fallback.
"Atomics.wait cannot be called in this context"
Atomics.wait only works in Workers. The library handles this internally — if you see this error, you're likely calling sync methods from the main thread without proper COOP/COEP headers.
Files not visible in OPFS DevTools
Make sure opfsSync is enabled (it's true by default). Files are mirrored to OPFS in the background after each VFS operation. Check DevTools > Application > Storage > OPFS.
External OPFS changes not detected
FileSystemObserver requires Chrome 129+. The VFS instance must be running (observer is set up during init). Changes to files outside the configured root directory won't be detected.
Changelog
v3.0.19-v3.0.20 (2026)
Features:
- Add
swUrlconfig option to specify a custom service worker URL for multi-tab support in bundled environments where the default auto-resolved URL doesn't work - Remove
type: 'module'from service worker registration (built output is plain script, not ESM)
v3.0.18 (2026)
Features:
- Configurable VFS limits via
limitsoption:maxInodes,maxBlocks,maxPathTable,maxVFSSize,maxPayload
Fixes:
- Pre-validate superblock before
engine.init()to prevent hangs from corrupt values causing huge allocations - Add upper bounds in
mount(): max 4M inodes, 4M blocks, 256MB path table, 100GB total VFS size - Ensure all mount errors are prefixed with
Corrupt VFS:for consistent corruption fallback - Cap
readPayload()at 2GB (configurable) and validate each chunk length in the multi-chunk loop to prevent OOM/infinite loops from corrupt SAB data - Cap
MemoryHandle.grow()at 4GB to prevent OOM from corrupt VFS offsets on main-thread fallback
v3.0.17 (2026)
Features:
- Auto-populate VFS from existing OPFS files when
.vfs.bindoesn't exist — seamless transition from OPFS mode back to hybrid mode without manualloadFromOPFS()call
v3.0.16 (2026)
Fixes:
- Add 10s timeout to main-thread spin-wait — prevents infinite busy loop if SharedWorker is dead or unresponsive
v3.0.15 (2026)
Fixes:
- Add bounds validation to
decodeRequest— rejects truncated SAB payloads instead of reading out-of-bounds - Wrap
decodeRequestin try/catch in both VFS and OPFS leader loop handlers — corrupt buffers returnstatus: -1instead of crashing the leader loop - Guard
readPayloadagainst zero/negative/overflow chunk lengths from stale SAB data
v3.0.14 (2026)
Fixes:
- Fix
PATH_USEDnot persisted afterwrite()without flush flag —commitPending()now runs unconditionally after every write, preventing "path out of bounds" corruption on reload - Repair inode scanner uses the full allocated path table region instead of
PATH_USEDfrom the superblock — recovers files even when the superblock counter was stale
v3.0.13 (2026)
Fixes:
- Revert
.d.tsextension override inoutExtension—dts: truehandles it correctly
v3.0.12 (2026)
Fixes:
- Fix
.d.tsoutput extension mapping so declaration files resolve correctly
v3.0.11 (2026)
Fixes:
- Emit TypeScript declaration files (
dts: truein tsup config)
v3.0.10 (2026)
New: Three filesystem modes (hybrid, vfs, opfs)
mode: 'hybrid'(default) — VFS binary + bidirectional OPFS syncmode: 'vfs'— VFS binary only, no OPFS mirroring (fastest)mode: 'opfs'— Pure OPFS files, no VFS binary (most resilient)- New
OPFSEngineimplements all fs operations directly on OPFS files
Automatic corruption fallback
- Hybrid mode auto-falls back to OPFS mode on VFS corruption
await fs.init()rejects with descriptive error while system works in OPFS modefs.modegetter reflects current mode (changes to'opfs'on fallback)
Runtime mode switching
await fs.setMode('hybrid' | 'vfs' | 'opfs')for switching modes at runtime- IDE workflow: corruption → OPFS fallback → repair →
setMode('hybrid')
Corruption detection improvements
rebuildIndex()validates every inode: type, path bounds, data block range, path format- Fixed
format()not persistingpathTableUsedafter root inode creation
Repair safety
- Dedicated repair worker with
createSyncAccessHandle— no RAM bloat - Original
.vfs.binis never deleted until replacement is verified via re-mount - Copy-then-delete swap: crash mid-copy leaves
.vfs.bin.tmpintact for retry loadFromOPFSbuilds in temp file first — original untouched until verified- Strict UTF-8 decoding for recovered paths and symlink targets (rejects invalid sequences)
contentLostflag on repair entries distinguishes empty files from files with lost data- Repair aborts after 5 critical
mkdirfailures (fail-fast threshold) - Orphaned
.vfs.bin.tmpfiles cleaned up automatically on repair entry
v3.0.9 (2026)
Improvements:
unpackToOPFS,loadFromOPFS, andrepairVFSnow accept an optionalfsparameter (a runningVFSFileSysteminstance) so they work from any tab — leader or follower — without stopping the VFS- When
fsis not provided, falls back to direct.vfs.binaccess via VFSEngine (requires VFS to be stopped or a Worker context) repairVFSwith a running instance uses OPFS as source of truth: rebuilds VFS from OPFS, then syncs back for full consistency
v3.0.8 (2026)
Improvements:
- Add VFS helper functions:
unpackToOPFS,loadFromOPFS, andrepairVFSfor VFS maintenance, migration, and recovery - Helpers work in both Worker (sync access handle) and main thread (in-memory buffer + async writable) contexts
- Remove redundant I/O call in
unpackToOPFSdirectory creation
v3.0.7 (2026)
Fixes:
- Fix
fs.watch()path matching for root/watchers — watching/now correctly matches all child paths instead of missing them due to an off-by-one boundary check
v3.0.6 (2026)
Performance:
- Bulk-read inode + path tables during mount — 2 I/O calls instead of 10,000+, dramatically faster initialization for large VFS files
- All active inodes pre-populated in cache on mount (no cold-read penalty for first operations)
Fixes:
.vfs.binnow auto-shrinks: trailing free blocks are trimmed on every commit, reclaiming disk space when files are deleted- Minimum of 1024 data blocks (4MB) retained to avoid excessive re-growth on small create/delete cycles
v3.0.5 (2026)
Fixes:
- Scope the internal service worker by default so it won't collide with the host application's own service worker
- Remove unnecessary
clients.claim()from the service worker — it only acts as a MessagePort broker and never needs to control pages - Namespace leader lock, BroadcastChannel, and SW scope by
rootso multipleVFSFileSysteminstances with different roots don't collide - Add
swScopeconfig option for custom service worker scope override - Singleton registry: multiple
new VFSFileSystem()calls with the same root return the same instance (no duplicate workers) - Namespace
vfs-watchBroadcastChannel by root so watch events don't leak between different roots
v3.0.4 (2026)
Features:
- Add
unpackToOPFS(root?)— export all VFS contents to real OPFS files - Add
loadFromOPFS(root?)— rebuild VFS from real OPFS files (deletes and recreates.vfs.bin) - Add
repairVFS(root?)— scan corrupt VFS binary for recoverable inodes and rebuild a clean VFS - Add
VFSEngine.exportAll()for extracting all files/dirs/symlinks with their data
Bug Fixes:
- VFS corruption detection on init — validates magic, version, block size, inode count, section offsets, file size, and root directory existence
- Release sync access handle on init failure (previously leaked, blocking re-acquisition)
v3.0.3 (2026)
Features:
- Implement
fs.watch(),fs.watchFile(),fs.unwatchFile(), andpromises.watch()as Node.js-compatible polyfills - Watch events propagate across all tabs via
BroadcastChannel fs.watch()supportsrecursiveoption andAbortSignalfor cleanupfs.watchFile()supports stat-based polling with configurableinterval(default 5007ms per Node.js)promises.watch()returns an async iterable of watch events
Internal:
- Leader broadcasts
{ eventType, path }on every successful VFS mutation (no new opcodes or protocol changes) - Mutation tracking now runs unconditionally (previously gated on
opfsSync)
v3.0.2 (2026)
Bug Fixes:
- Fix symlink resolution when resolved target path contains intermediate symlinks —
resolvePathnow falls back to component-by-component resolution instead of failing on direct lookup - Add ELOOP depth tracking to
resolvePathComponentsto prevent infinite recursion on circular symlinks - Mirror symlinks to OPFS as regular files (OPFS has no symlink concept) — reads through the symlink and writes the target's content
v3.0.1 (2026)
Bug Fixes:
- Fix empty files (e.g.
.gitkeep) not being mirrored to OPFS — both the sync-relay (skipped sending empty data) and opfs-sync worker (skipped writing 0-byte files) now handle empty files correctly
Benchmark:
- Add memfs (in-memory) to the benchmark suite for comparison
v3.0.0 (2026)
Complete architecture rewrite — VFS binary format with SharedArrayBuffer.
New Architecture:
- VFS binary format — all data stored in a single
.vfs.binfile (Superblock → Inode Table → Path Table → Bitmap → Data Region) - SharedArrayBuffer + Atomics for true zero-overhead synchronous operations
- Multi-tab leader/follower architecture with automatic failover via
navigator.locks+ Service Worker - Bidirectional OPFS sync — VFS mutations mirrored to real OPFS files, external changes synced back via
FileSystemObserver - Workers inlined as blob URLs at build time (zero config, no external worker files)
- Echo suppression for OPFS sync (prevents infinite sync loops)
Performance:
- 9-28x faster reads vs LightningFS
- 2.8-4x faster writes vs LightningFS
- 2.8-10x faster batch operations vs LightningFS
- Fire-and-forget OPFS sync — zero impact on hot path
Breaking Changes:
- New API:
new VFSFileSystem(config)instead of defaultfssingleton createFS(config)andgetDefaultFS()helpers available- Requires
crossOriginIsolatedfor sync API (async API works everywhere) - Complete internal rewrite — not backwards compatible with v2 internals
v2.0.0 (2025)
Major rewrite with sync API support via OPFS sync access handles and performance tiers.
v1.0.0 (2024)
Initial release — async-only OPFS filesystem with fs.promises API.
Contributing
git clone https://github.com/componentor/fs
cd fs
npm install
npm run build # Build the library
npm test # Run unit tests (107 tests)
npm run benchmark:open # Run benchmarks in browserLicense
MIT
