@componentor/fs 2.0.12 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,13 +1,15 @@
1
1
  # @componentor/fs
2
2
 
3
- **Battle-tested OPFS-based Node.js `fs` polyfill with sync and async APIs**
3
+ **High-performance OPFS-based Node.js `fs` polyfill for the browser**
4
4
 
5
- A high-performance browser filesystem with native OPFS backend and synchronous API support.
5
+ 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.
6
6
 
7
7
  ```typescript
8
- import { fs } from '@componentor/fs';
8
+ import { VFSFileSystem } from '@componentor/fs';
9
9
 
10
- // Sync API (requires crossOriginIsolated)
10
+ const fs = new VFSFileSystem();
11
+
12
+ // Sync API (requires crossOriginIsolated — blocks until ready on first call)
11
13
  fs.writeFileSync('/hello.txt', 'Hello World!');
12
14
  const data = fs.readFileSync('/hello.txt', 'utf8');
13
15
 
@@ -18,13 +20,15 @@ const content = await fs.promises.readFile('/async.txt', 'utf8');
18
20
 
19
21
  ## Features
20
22
 
21
- - **Node.js Compatible** - Drop-in replacement for `fs` module
22
- - **Sync API** - `readFileSync`, `writeFileSync`, etc. (requires COOP/COEP)
23
- - **Async API** - `promises.readFile`, `promises.writeFile`, etc.
24
- - **Cross-tab Safe** - Uses `navigator.locks` for multi-tab coordination
25
- - **isomorphic-git Ready** - Full compatibility with git operations
26
- - **Zero Config** - Works out of the box, no worker files needed
27
- - **TypeScript First** - Complete type definitions included
23
+ - **True Sync API** `readFileSync`, `writeFileSync`, etc. via SharedArrayBuffer + Atomics
24
+ - **Async API** `promises.readFile`, `promises.writeFile` works without COOP/COEP
25
+ - **VFS Binary Format** All data in a single `.vfs.bin` file for maximum throughput
26
+ - **OPFS Sync** Bidirectional mirror to real OPFS files (enabled by default)
27
+ - **Multi-tab Safe** Leader/follower architecture with automatic failover via `navigator.locks`
28
+ - **FileSystemObserver** External OPFS changes synced back to VFS automatically (Chrome 129+)
29
+ - **isomorphic-git Ready** Full compatibility with git operations
30
+ - **Zero Config** — Workers inlined at build time, no external worker files needed
31
+ - **TypeScript First** — Complete type definitions included
28
32
 
29
33
  ## Installation
30
34
 
@@ -35,72 +39,83 @@ npm install @componentor/fs
35
39
  ## Quick Start
36
40
 
37
41
  ```typescript
38
- import { fs, path } from '@componentor/fs';
39
-
40
- // Create a directory
41
- await fs.promises.mkdir('/projects/my-app', { recursive: true });
42
-
43
- // Write a file
44
- await fs.promises.writeFile('/projects/my-app/index.js', 'console.log("Hello!");');
42
+ import { VFSFileSystem } from '@componentor/fs';
45
43
 
46
- // Read a file
47
- const code = await fs.promises.readFile('/projects/my-app/index.js', 'utf8');
48
- console.log(code); // 'console.log("Hello!");'
44
+ const fs = new VFSFileSystem({ root: '/my-app' });
49
45
 
50
- // List directory contents
51
- const files = await fs.promises.readdir('/projects/my-app');
52
- console.log(files); // ['index.js']
46
+ // Option 1: Sync API (blocks on first call until VFS is ready)
47
+ fs.mkdirSync('/my-app/src', { recursive: true });
48
+ fs.writeFileSync('/my-app/src/index.js', 'console.log("Hello!");');
49
+ const code = fs.readFileSync('/my-app/src/index.js', 'utf8');
53
50
 
54
- // Get file stats
55
- const stats = await fs.promises.stat('/projects/my-app/index.js');
56
- console.log(stats.size); // 23
57
-
58
- // Use path utilities
59
- console.log(path.join('/projects', 'my-app', 'src')); // '/projects/my-app/src'
60
- console.log(path.dirname('/projects/my-app/index.js')); // '/projects/my-app'
61
- console.log(path.basename('/projects/my-app/index.js')); // 'index.js'
51
+ // Option 2: Async init (non-blocking)
52
+ await fs.init(); // wait for VFS to be ready
53
+ const files = await fs.promises.readdir('/my-app/src');
54
+ const stats = await fs.promises.stat('/my-app/src/index.js');
62
55
  ```
63
56
 
64
- ## Performance Tiers
57
+ ### Convenience Helpers
65
58
 
66
- @componentor/fs operates in two performance tiers based on browser capabilities:
59
+ ```typescript
60
+ import { createFS, getDefaultFS, init } from '@componentor/fs';
67
61
 
68
- ### Tier 1: Sync (Fastest)
62
+ // Create with config
63
+ const fs = createFS({ root: '/repo', debug: true });
69
64
 
70
- **Requirements:** `crossOriginIsolated` context (COOP/COEP headers)
65
+ // Lazy singleton (created on first access)
66
+ const defaultFs = getDefaultFS();
71
67
 
72
- Uses `SharedArrayBuffer` + `Atomics` for zero-copy data transfer between main thread and worker. Enables **synchronous** filesystem operations.
68
+ // Async init helper
69
+ await init(); // initializes the default singleton
70
+ ```
71
+
72
+ ## Configuration
73
73
 
74
74
  ```typescript
75
- // Tier 1 unlocks sync APIs
76
- fs.writeFileSync('/file.txt', 'data');
77
- const data = fs.readFileSync('/file.txt', 'utf8');
78
- fs.mkdirSync('/dir', { recursive: true });
79
- fs.existsSync('/file.txt'); // true
75
+ const fs = new VFSFileSystem({
76
+ root: '/', // OPFS root directory (default: '/')
77
+ opfsSync: true, // Mirror VFS to real OPFS files (default: true)
78
+ opfsSyncRoot: undefined, // Custom OPFS root for mirroring (default: same as root)
79
+ uid: 0, // User ID for file ownership (default: 0)
80
+ gid: 0, // Group ID for file ownership (default: 0)
81
+ umask: 0o022, // File creation mask (default: 0o022)
82
+ strictPermissions: false, // Enforce Unix permissions (default: false)
83
+ sabSize: 4194304, // SharedArrayBuffer size in bytes (default: 4MB)
84
+ debug: false, // Enable debug logging (default: false)
85
+ });
80
86
  ```
81
87
 
82
- ### Tier 2: Async (Always Available)
88
+ ### OPFS Sync
89
+
90
+ When `opfsSync` is enabled (the default), VFS mutations are mirrored to real OPFS files in the background:
91
+
92
+ - **VFS → OPFS**: Every write, delete, mkdir, rename is replicated to real OPFS files after the sync operation completes (zero performance impact on the hot path)
93
+ - **OPFS → VFS**: A `FileSystemObserver` watches for external changes and syncs them back (Chrome 129+)
83
94
 
84
- Works in any browser context without special headers. Uses Web Worker with `postMessage` for async operations.
95
+ This allows external tools (browser DevTools, OPFS extensions) to see and modify files while VFS handles all the fast read/write operations internally.
85
96
 
86
97
  ```typescript
87
- // Tier 2 - promises API always works
88
- await fs.promises.writeFile('/file.txt', 'data');
89
- const data = await fs.promises.readFile('/file.txt', 'utf8');
90
- await fs.promises.mkdir('/dir', { recursive: true });
91
- await fs.promises.exists('/file.txt'); // true
98
+ // OPFS sync enabled (default)
99
+ const fs = new VFSFileSystem({ opfsSync: true });
100
+ fs.writeFileSync('/file.txt', 'data');
101
+ // → /file.txt also appears in OPFS (visible in DevTools > Application > Storage)
102
+
103
+ // Disable for maximum performance (no OPFS mirroring)
104
+ const fastFs = new VFSFileSystem({ opfsSync: false });
92
105
  ```
93
106
 
94
- ## COOP/COEP Headers (Required for Tier 1)
107
+ ## COOP/COEP Headers
95
108
 
96
- To enable Tier 1 (sync) performance, your server must send these headers:
109
+ To enable the sync API, your page must be `crossOriginIsolated`. Add these headers:
97
110
 
98
111
  ```
99
112
  Cross-Origin-Opener-Policy: same-origin
100
113
  Cross-Origin-Embedder-Policy: require-corp
101
114
  ```
102
115
 
103
- ### Vite Configuration
116
+ Without these headers, only the async (`promises`) API is available.
117
+
118
+ ### Vite
104
119
 
105
120
  ```typescript
106
121
  // vite.config.ts
@@ -114,7 +129,7 @@ export default defineConfig({
114
129
  });
115
130
  ```
116
131
 
117
- ### Express/Node.js
132
+ ### Express
118
133
 
119
134
  ```javascript
120
135
  app.use((req, res, next) => {
@@ -127,7 +142,6 @@ app.use((req, res, next) => {
127
142
  ### Vercel
128
143
 
129
144
  ```json
130
- // vercel.json
131
145
  {
132
146
  "headers": [
133
147
  {
@@ -141,41 +155,37 @@ app.use((req, res, next) => {
141
155
  }
142
156
  ```
143
157
 
144
- ### Check if Tier 1 is Available
158
+ ### Runtime Check
145
159
 
146
160
  ```typescript
147
161
  if (crossOriginIsolated) {
148
- console.log('Tier 1 (sync) available!');
162
+ // Sync + async APIs available
149
163
  fs.writeFileSync('/fast.txt', 'blazing fast');
150
164
  } else {
151
- console.log('Tier 2 (async) only');
165
+ // Async API only
152
166
  await fs.promises.writeFile('/fast.txt', 'still fast');
153
167
  }
154
168
  ```
155
169
 
156
170
  ## Benchmarks
157
171
 
158
- Tested against LightningFS (IndexedDB-based filesystem) in Chrome with Tier 1 enabled:
172
+ Tested against LightningFS (IndexedDB-based) in Chrome with `crossOriginIsolated` enabled:
159
173
 
160
- | Operation | @componentor/fs | LightningFS | Winner |
161
- |-----------|-----------------|-------------|--------|
162
- | Write 100 x 1KB | 131ms (763 ops/s) | 317ms (316 ops/s) | **OPFS 2.4x** |
163
- | Write 100 x 4KB | 145ms (690 ops/s) | 49ms (2061 ops/s) | LightningFS |
164
- | Read 100 x 1KB | 11ms (9170 ops/s) | 17ms (5824 ops/s) | **OPFS 1.6x** |
165
- | Read 100 x 4KB | 10ms (10493 ops/s) | 16ms (6431 ops/s) | **OPFS 1.6x** |
166
- | Large 10 x 1MB | 19ms (538 ops/s) | 11ms (910 ops/s) | LightningFS |
167
- | Batch Write 500 | 416ms (1202 ops/s) | 125ms (4014 ops/s) | LightningFS |
168
- | Batch Read 500 | 311ms (1608 ops/s) | 74ms (6736 ops/s) | LightningFS |
169
- | **Git Clone** | 427ms | 1325ms | **OPFS 3.1x** |
170
- | Git Status 10x | 53ms | 39ms | LightningFS |
174
+ | Operation | LightningFS | VFS Sync | VFS Promises | Winner |
175
+ |-----------|------------|----------|-------------|--------|
176
+ | Write 100 x 1KB | 46ms | **12ms** | 23ms | **VFS 4x** |
177
+ | Write 100 x 4KB | 36ms | **13ms** | 22ms | **VFS 2.8x** |
178
+ | Read 100 x 1KB | 19ms | **2ms** | 14ms | **VFS 9x** |
179
+ | Read 100 x 4KB | 62ms | **2ms** | 13ms | **VFS 28x** |
180
+ | Large 10 x 1MB | 11ms | **10ms** | 17ms | **VFS 1.1x** |
181
+ | Batch Write 500 x 256B | 138ms | **50ms** | 75ms | **VFS 2.8x** |
182
+ | Batch Read 500 x 256B | 73ms | **7ms** | 91ms | **VFS 10x** |
171
183
 
172
184
  **Key takeaways:**
173
- - **Git clone is 2-3x faster** - the most important real-world operation
174
- - **Reads are 1.6x faster** - OPFS excels at read operations
175
- - **Small writes (1KB) are 2.4x faster** - great for config files and metadata
176
- - LightningFS wins on batch operations and larger sequential writes
177
-
178
- *Results from Chrome 120+ with crossOriginIsolated enabled. Performance varies by browser and hardware.*
185
+ - **Reads are 9-28x faster** VFS binary format eliminates IndexedDB overhead
186
+ - **Writes are 2.8-4x faster** Single binary file vs individual OPFS/IDB entries
187
+ - **Batch operations are 2.8-10x faster** VFS excels at many small operations
188
+ - VFS Sync is the fastest path (SharedArrayBuffer + Atomics, zero async overhead)
179
189
 
180
190
  Run benchmarks yourself:
181
191
 
@@ -185,114 +195,149 @@ npm run benchmark:open
185
195
 
186
196
  ## API Reference
187
197
 
188
- ### Sync API (Tier 1 Only)
198
+ ### Sync API (requires crossOriginIsolated)
189
199
 
190
200
  ```typescript
191
201
  // Read/Write
192
- fs.readFileSync(path: string, options?: { encoding?: string }): Uint8Array | string
193
- fs.writeFileSync(path: string, data: Uint8Array | string, options?: { flush?: boolean }): void
194
- fs.appendFileSync(path: string, data: Uint8Array | string): void
202
+ fs.readFileSync(path, options?): Uint8Array | string
203
+ fs.writeFileSync(path, data, options?): void
204
+ fs.appendFileSync(path, data): void
195
205
 
196
206
  // Directories
197
- fs.mkdirSync(path: string, options?: { recursive?: boolean }): void
198
- fs.rmdirSync(path: string, options?: { recursive?: boolean }): void
199
- fs.readdirSync(path: string): string[]
207
+ fs.mkdirSync(path, options?): void
208
+ fs.rmdirSync(path, options?): void
209
+ fs.rmSync(path, options?): void
210
+ fs.readdirSync(path, options?): string[] | Dirent[]
200
211
 
201
212
  // File Operations
202
- fs.unlinkSync(path: string): void
203
- fs.renameSync(oldPath: string, newPath: string): void
204
- fs.copyFileSync(src: string, dest: string): void
205
- fs.truncateSync(path: string, len?: number): void
213
+ fs.unlinkSync(path): void
214
+ fs.renameSync(oldPath, newPath): void
215
+ fs.copyFileSync(src, dest, mode?): void
216
+ fs.truncateSync(path, len?): void
217
+ fs.symlinkSync(target, path): void
218
+ fs.readlinkSync(path): string
219
+ fs.linkSync(existingPath, newPath): void
206
220
 
207
221
  // Info
208
- fs.statSync(path: string): Stats
209
- fs.existsSync(path: string): boolean
210
- fs.accessSync(path: string, mode?: number): void
222
+ fs.statSync(path): Stats
223
+ fs.lstatSync(path): Stats
224
+ fs.existsSync(path): boolean
225
+ fs.accessSync(path, mode?): void
226
+ fs.realpathSync(path): string
227
+
228
+ // Metadata
229
+ fs.chmodSync(path, mode): void
230
+ fs.chownSync(path, uid, gid): void
231
+ fs.utimesSync(path, atime, mtime): void
232
+
233
+ // File Descriptors
234
+ fs.openSync(path, flags?, mode?): number
235
+ fs.closeSync(fd): void
236
+ fs.readSync(fd, buffer, offset?, length?, position?): number
237
+ fs.writeSync(fd, buffer, offset?, length?, position?): number
238
+ fs.fstatSync(fd): Stats
239
+ fs.ftruncateSync(fd, len?): void
240
+ fs.fdatasyncSync(fd): void
241
+
242
+ // Temp / Flush
243
+ fs.mkdtempSync(prefix): string
244
+ fs.flushSync(): void
211
245
  ```
212
246
 
213
- ### Async API (Always Available)
247
+ ### Async API (always available)
214
248
 
215
249
  ```typescript
216
250
  // Read/Write
217
- fs.promises.readFile(path: string, options?: ReadOptions): Promise<Uint8Array | string>
218
- fs.promises.writeFile(path: string, data: Uint8Array | string, options?: WriteOptions): Promise<void>
219
- fs.promises.appendFile(path: string, data: Uint8Array | string): Promise<void>
251
+ fs.promises.readFile(path, options?): Promise<Uint8Array | string>
252
+ fs.promises.writeFile(path, data, options?): Promise<void>
253
+ fs.promises.appendFile(path, data): Promise<void>
220
254
 
221
255
  // Directories
222
- fs.promises.mkdir(path: string, options?: { recursive?: boolean }): Promise<void>
223
- fs.promises.rmdir(path: string, options?: { recursive?: boolean }): Promise<void>
224
- fs.promises.readdir(path: string, options?: { withFileTypes?: boolean }): Promise<string[] | Dirent[]>
256
+ fs.promises.mkdir(path, options?): Promise<void>
257
+ fs.promises.rmdir(path, options?): Promise<void>
258
+ fs.promises.rm(path, options?): Promise<void>
259
+ fs.promises.readdir(path, options?): Promise<string[] | Dirent[]>
225
260
 
226
261
  // File Operations
227
- fs.promises.unlink(path: string): Promise<void>
228
- fs.promises.rename(oldPath: string, newPath: string): Promise<void>
229
- fs.promises.copyFile(src: string, dest: string): Promise<void>
230
- fs.promises.truncate(path: string, len?: number): Promise<void>
231
- fs.promises.rm(path: string, options?: { recursive?: boolean, force?: boolean }): Promise<void>
262
+ fs.promises.unlink(path): Promise<void>
263
+ fs.promises.rename(oldPath, newPath): Promise<void>
264
+ fs.promises.copyFile(src, dest, mode?): Promise<void>
265
+ fs.promises.truncate(path, len?): Promise<void>
266
+ fs.promises.symlink(target, path): Promise<void>
267
+ fs.promises.readlink(path): Promise<string>
268
+ fs.promises.link(existingPath, newPath): Promise<void>
232
269
 
233
270
  // Info
234
- fs.promises.stat(path: string): Promise<Stats>
235
- fs.promises.lstat(path: string): Promise<Stats>
236
- fs.promises.exists(path: string): Promise<boolean>
237
- fs.promises.access(path: string, mode?: number): Promise<void>
238
- fs.promises.realpath(path: string): Promise<string>
271
+ fs.promises.stat(path): Promise<Stats>
272
+ fs.promises.lstat(path): Promise<Stats>
273
+ fs.promises.exists(path): Promise<boolean>
274
+ fs.promises.access(path, mode?): Promise<void>
275
+ fs.promises.realpath(path): Promise<string>
276
+
277
+ // Metadata
278
+ fs.promises.chmod(path, mode): Promise<void>
279
+ fs.promises.chown(path, uid, gid): Promise<void>
280
+ fs.promises.utimes(path, atime, mtime): Promise<void>
239
281
 
240
282
  // Advanced
241
- fs.promises.open(path: string, flags?: string, mode?: number): Promise<FileHandle>
242
- fs.promises.opendir(path: string): Promise<Dir>
243
- fs.promises.mkdtemp(prefix: string): Promise<string>
244
- fs.promises.symlink(target: string, path: string): Promise<void>
245
- fs.promises.readlink(path: string): Promise<string>
246
- fs.promises.link(existingPath: string, newPath: string): Promise<void>
247
-
248
- // Cache Management
249
- fs.promises.flush(): Promise<void> // Flush pending writes
250
- fs.promises.purge(): Promise<void> // Clear all caches
283
+ fs.promises.open(path, flags?, mode?): Promise<FileHandle>
284
+ fs.promises.opendir(path): Promise<Dir>
285
+ fs.promises.mkdtemp(prefix): Promise<string>
286
+
287
+ // Flush
288
+ fs.promises.flush(): Promise<void>
251
289
  ```
252
290
 
253
291
  ### Streams API
254
292
 
255
293
  ```typescript
256
- // Create a readable stream (Web Streams API)
257
- fs.createReadStream(path: string, options?: {
258
- start?: number, // Byte offset to start reading
259
- end?: number, // Byte offset to stop reading
260
- highWaterMark?: number // Chunk size (default: 64KB)
261
- }): ReadableStream<Uint8Array>
262
-
263
- // Create a writable stream (Web Streams API)
264
- fs.createWriteStream(path: string, options?: {
265
- start?: number, // Byte offset to start writing
266
- flush?: boolean // Flush on close (default: true)
267
- }): WritableStream<Uint8Array>
268
-
269
- // Example: Stream a file
270
- const stream = fs.createReadStream('/large-file.bin');
294
+ // Readable stream (Web Streams API)
295
+ const stream = fs.createReadStream('/large-file.bin', {
296
+ start: 0, // byte offset to start
297
+ end: 1024, // byte offset to stop
298
+ highWaterMark: 64 * 1024, // chunk size (default: 64KB)
299
+ });
271
300
  for await (const chunk of stream) {
272
301
  console.log('Read chunk:', chunk.length, 'bytes');
273
302
  }
274
303
 
275
- // Example: Write with streams
304
+ // Writable stream
276
305
  const writable = fs.createWriteStream('/output.bin');
277
306
  const writer = writable.getWriter();
278
307
  await writer.write(new Uint8Array([1, 2, 3]));
279
308
  await writer.close();
280
309
  ```
281
310
 
311
+ ### Watch API
312
+
313
+ ```typescript
314
+ // Watch for changes
315
+ const watcher = fs.watch('/dir', { recursive: true }, (eventType, filename) => {
316
+ console.log(eventType, filename); // 'change' 'file.txt'
317
+ });
318
+ watcher.close();
319
+
320
+ // Watch specific file with polling
321
+ fs.watchFile('/file.txt', { interval: 1000 }, (curr, prev) => {
322
+ console.log('File changed:', curr.mtimeMs !== prev.mtimeMs);
323
+ });
324
+ fs.unwatchFile('/file.txt');
325
+ ```
326
+
282
327
  ### Path Utilities
283
328
 
284
329
  ```typescript
285
330
  import { path } from '@componentor/fs';
286
331
 
287
- path.join('/foo', 'bar', 'baz') // '/foo/bar/baz'
288
- path.resolve('foo', 'bar') // '/foo/bar'
289
- path.dirname('/foo/bar/baz.txt') // '/foo/bar'
290
- path.basename('/foo/bar/baz.txt') // 'baz.txt'
291
- path.extname('/foo/bar/baz.txt') // '.txt'
292
- path.normalize('/foo//bar/../baz') // '/foo/baz'
293
- path.isAbsolute('/foo') // true
332
+ path.join('/foo', 'bar', 'baz') // '/foo/bar/baz'
333
+ path.resolve('foo', 'bar') // '/foo/bar'
334
+ path.dirname('/foo/bar/baz.txt') // '/foo/bar'
335
+ path.basename('/foo/bar/baz.txt') // 'baz.txt'
336
+ path.extname('/foo/bar/baz.txt') // '.txt'
337
+ path.normalize('/foo//bar/../baz') // '/foo/baz'
338
+ path.isAbsolute('/foo') // true
294
339
  path.relative('/foo/bar', '/foo/baz') // '../baz'
295
- path.parse('/foo/bar/baz.txt') // { root, dir, base, ext, name }
340
+ path.parse('/foo/bar/baz.txt') // { root, dir, base, ext, name }
296
341
  path.format({ dir: '/foo', name: 'bar', ext: '.txt' }) // '/foo/bar.txt'
297
342
  ```
298
343
 
@@ -319,13 +364,13 @@ constants.O_APPEND // 1024
319
364
 
320
365
  ## isomorphic-git Integration
321
366
 
322
- @componentor/fs works seamlessly with isomorphic-git:
323
-
324
367
  ```typescript
325
- import { fs } from '@componentor/fs';
368
+ import { VFSFileSystem } from '@componentor/fs';
326
369
  import git from 'isomorphic-git';
327
370
  import http from 'isomorphic-git/http/web';
328
371
 
372
+ const fs = new VFSFileSystem({ root: '/repo' });
373
+
329
374
  // Clone a repository
330
375
  await git.clone({
331
376
  fs,
@@ -351,93 +396,52 @@ await git.commit({
351
396
  ## Architecture
352
397
 
353
398
  ```
354
- ┌─────────────────────────────────────────────────────────────┐
355
- Main Thread
356
- ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐
357
- │ │ Sync API │ │ Async API │ │ Path Utilities │ │
358
- │ │ readFileSync│ │ promises. │ │ join, dirname, etc. │ │
359
- │ │writeFileSync│ │ readFile └─────────────────────┘
360
- └──────┬──────┘ └──────┬──────┘
361
- │ │
362
- Atomics.wait │ postMessage
363
- │ │ (Tier 1) │ (Tier 2) │
364
- └─────────┼────────────────┼──────────────────────────────────┘
365
- │ │
366
- ▼ ▼
367
- ┌─────────────────────────────────────────────────────────────┐
368
- Web Worker
369
- ┌────────────────────────────────────────────────────────┐
370
- │ │ OPFS Kernel
371
- │ │ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │ │
372
- │ │ │ Sync Handle │ │ Directory │ │ navigator │ │
373
- │ │ │ Cache │ Cache │ .locks │ │ │
374
- (100 max) │ │ │ │ (cross-tab) │ │ │
375
- └──────────────┘ └──────────────┘ └─────────────┘
376
- └────────────────────────────────────────────────────────┘
377
-
378
- └────────────────────────────┼────────────────────────────────┘
399
+ ┌──────────────────────────────────────────────────────────────────┐
400
+ Main Thread
401
+ ┌──────────────┐ ┌──────────────┐ ┌────────────────────────┐
402
+ │ │ Sync API │ │ Async API │ │ Path / Constants │ │
403
+ │ │ readFileSync │ │ promises. │ │ join, dirname, etc. │ │
404
+ │ │writeFileSync │ │ readFile └────────────────────────┘
405
+ └──────┬───────┘ └──────┬───────┘
406
+ │ │
407
+ SAB + Atomics postMessage
408
+ └─────────┼─────────────────┼──────────────────────────────────────┘
409
+ │ │
410
+ ▼ ▼
411
+ ┌──────────────────────────────────────────────────────────────────┐
412
+ │ sync-relay Worker (Leader) │
413
+ ┌────────────────────────────────────────────────────────────┐
414
+ │ VFS Engine
415
+ │ │ ┌──────────────────┐ ┌─────────────┐ ┌──────────────┐
416
+ │ │ VFS Binary File │ Inode/Path │ Block Data │ │ │
417
+ │ │ │ (.vfs.bin OPFS) │ │ Table │ Region │ │ │
418
+ │ │ └──────────────────┘ └─────────────┘ └──────────────┘ │ │
419
+ └────────────────────────────────────────────────────────────┘
420
+
421
+ notifyOPFSSync()
422
+ (fire & forget)
423
+ └────────────────────────────┼─────────────────────────────────────┘
379
424
 
380
425
 
381
- ┌─────────────────────────────────────────────────────────────┐
382
- OPFS
383
- Origin Private File System
384
- (Browser Storage API)
385
- └─────────────────────────────────────────────────────────────┘
426
+ ┌──────────────────────────────────────────────────────────────────┐
427
+ opfs-sync Worker
428
+ ┌────────────────────┐ ┌────────────────────────────────────┐
429
+ │ VFS → OPFS Mirror │ │ FileSystemObserver (OPFS VFS)
430
+ │ │ (queue + echo │ │ External changes detected and │ │
431
+ │ │ suppression) │ │ synced back to VFS engine │ │
432
+ │ └────────────────────┘ └────────────────────────────────────┘ │
433
+ └──────────────────────────────────────────────────────────────────┘
434
+
435
+ Multi-tab (via Service Worker + navigator.locks):
436
+ Tab 1 (Leader) ←→ Service Worker ←→ Tab 2 (Follower)
437
+ Tab 1 holds VFS engine, Tab 2 forwards requests via MessagePort
438
+ If Tab 1 dies, Tab 2 auto-promotes to leader
386
439
  ```
387
440
 
388
- ## Feature Comparison
389
-
390
- ### API Compatibility
391
-
392
- | Feature | Node.js fs | @componentor/fs v2 | @componentor/fs v1 | LightningFS |
393
- |---------|------------|--------------------|--------------------|-------------|
394
- | `readFile` | ✅ | ✅ | ✅ | ✅ |
395
- | `writeFile` | ✅ | ✅ | ✅ | ✅ |
396
- | `readFileSync` | ✅ | ✅ Tier 1 | ❌ | ❌ |
397
- | `writeFileSync` | ✅ | ✅ Tier 1 | ❌ | ❌ |
398
- | `mkdir` / `mkdirSync` | ✅ | ✅ | ✅ | ✅ |
399
- | `readdir` / `readdirSync` | ✅ | ✅ | ✅ | ✅ |
400
- | `stat` / `statSync` | ✅ | ✅ | ✅ | ✅ |
401
- | `unlink` / `unlinkSync` | ✅ | ✅ | ✅ | ✅ |
402
- | `rename` / `renameSync` | ✅ | ✅ | ✅ | ✅ |
403
- | `rm` (recursive) | ✅ | ✅ | ✅ | ❌ |
404
- | `copyFile` | ✅ | ✅ | ✅ | ❌ |
405
- | `symlink` / `readlink` | ✅ | ✅ | ✅ | ✅ |
406
- | `watch` / `watchFile` | ✅ | ✅ | ❌ | ❌ |
407
- | `open` / `FileHandle` | ✅ | ✅ | ❌ | ❌ |
408
- | `opendir` / `Dir` | ✅ | ✅ | ❌ | ❌ |
409
- | `mkdtemp` | ✅ | ✅ | ❌ | ❌ |
410
- | Streams | ✅ | ✅ | ❌ | ❌ |
411
-
412
- ### Performance Tiers
413
-
414
- | Capability | Tier 1 Sync | Tier 1 Promises | Tier 2 | Legacy v1 | LightningFS |
415
- |------------|-------------|-----------------|--------|-----------|-------------|
416
- | **Sync API** | ✅ | ❌ | ❌ | ❌ | ❌ |
417
- | **Async API** | ✅ | ✅ | ✅ | ✅ | ✅ |
418
- | **Requires COOP/COEP** | ✅ | ✅ | ❌ | ❌ | ❌ |
419
- | **SharedArrayBuffer** | ✅ | ✅ | ❌ | ❌ | ❌ |
420
- | **Handle Caching** | ✅ | ✅ | ❌ | ❌ | N/A |
421
- | **Zero-copy Transfer** | ✅ | ❌ | ❌ | ❌ | ❌ |
422
- | **Cross-tab Safety** | ✅ | ✅ | ✅ | ✅ | ❌ |
423
- | **Storage Backend** | OPFS | OPFS | OPFS | OPFS | IndexedDB |
424
-
425
- ### Architecture Comparison
426
-
427
- | Aspect | @componentor/fs v2 | @componentor/fs v1 | LightningFS |
428
- |--------|--------------------|--------------------|-------------|
429
- | **Storage** | OPFS (native FS) | OPFS | IndexedDB |
430
- | **Worker** | Dedicated kernel | Shared worker | None |
431
- | **Sync Method** | Atomics.wait | N/A | N/A |
432
- | **Handle Strategy** | Cached (100 max) | Per-operation | N/A |
433
- | **Locking** | navigator.locks | navigator.locks | None |
434
- | **Bundle Size** | ~16KB | ~12KB | ~25KB |
435
- | **TypeScript** | Full | Full | Partial |
436
-
437
441
  ## Browser Support
438
442
 
439
- | Browser | Tier 1 (Sync) | Tier 2 (Async) |
440
- |---------|---------------|----------------|
443
+ | Browser | Sync API | Async API |
444
+ |---------|----------|-----------|
441
445
  | Chrome 102+ | Yes | Yes |
442
446
  | Edge 102+ | Yes | Yes |
443
447
  | Firefox 111+ | Yes* | Yes |
@@ -445,103 +449,71 @@ await git.commit({
445
449
  | Opera 88+ | Yes | Yes |
446
450
 
447
451
  \* Firefox requires `dom.workers.modules.enabled` flag
448
- \** Safari doesn't support `createSyncAccessHandle` in workers
452
+ \** Safari doesn't support `SharedArrayBuffer` in the required context
449
453
 
450
454
  ## Troubleshooting
451
455
 
452
456
  ### "SharedArrayBuffer is not defined"
453
457
 
454
- Your page is not crossOriginIsolated. Add COOP/COEP headers:
458
+ Your page is not `crossOriginIsolated`. Add COOP/COEP headers (see above). The async API still works without them.
455
459
 
456
- ```
457
- Cross-Origin-Opener-Policy: same-origin
458
- Cross-Origin-Embedder-Policy: require-corp
459
- ```
460
+ ### "Sync API requires crossOriginIsolated"
461
+
462
+ Same issue — sync methods (`readFileSync`, etc.) need `SharedArrayBuffer`. Use `fs.promises.*` as a fallback.
460
463
 
461
464
  ### "Atomics.wait cannot be called in this context"
462
465
 
463
- `Atomics.wait` can only be called from a Worker. The library handles this automatically - use the async API on the main thread.
466
+ `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.
464
467
 
465
- ### "NotAllowedError: Access handle is already open"
468
+ ### Files not visible in OPFS DevTools
466
469
 
467
- Another tab or operation has the file open. The library uses `navigator.locks` to prevent this, but if you're using multiple filesystem instances, ensure they coordinate.
470
+ 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.
468
471
 
469
- ### Slow Performance
472
+ ### External OPFS changes not detected
470
473
 
471
- 1. Check if Tier 1 is enabled: `console.log(crossOriginIsolated)`
472
- 2. Use batch operations when possible
473
- 3. Disable flush for bulk writes: `{ flush: false }`
474
- 4. Call `fs.promises.flush()` after bulk operations
474
+ `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.
475
475
 
476
476
  ## Changelog
477
477
 
478
- ### v2.0.11 (2026)
479
-
480
- **Document streams API:**
481
- - Update readme about available streams API
482
-
483
- ### v2.0.7 (2025)
484
-
485
- **High-Performance Handle Caching with `readwrite-unsafe`:**
486
- - Uses `readwrite-unsafe` mode (Chrome 121+) - no exclusive locks
487
- - Zero per-operation overhead: cache lookup is a single Map.get()
488
- - Browser extensions can access files while handles are cached
489
- - LRU eviction when cache exceeds 100 handles
490
- - Falls back to 100ms debounced release on older browsers (handles block)
491
-
492
- ### v2.0.2 (2025)
493
-
494
- **Improvements:**
495
- - Sync access handles now auto-release after idle timeout
496
- - Allows external tools (like OPFS Chrome extension) to access files when idle
497
- - Maintains full performance during active operations
498
-
499
- ### v2.0.1 (2025)
478
+ ### v3.0.1 (2026)
500
479
 
501
480
  **Bug Fixes:**
502
- - Fixed mtime not updating correctly when files are modified
503
- - `stat()` now always returns accurate `lastModified` from OPFS instead of approximation
504
- - Ensures git status and other mtime-dependent operations work correctly
481
+ - 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
505
482
 
506
- ### v2.0.0 (2025)
483
+ **Benchmark:**
484
+ - Add memfs (in-memory) to the benchmark suite for comparison
507
485
 
508
- **Major rewrite with sync API support and performance tiers.**
486
+ ### v3.0.0 (2026)
509
487
 
510
- **New Features:**
511
- - Synchronous API (`readFileSync`, `writeFileSync`, etc.) via Atomics
512
- - Performance tiers (Tier 1 Sync, Tier 1 Promises, Tier 2)
513
- - Dedicated worker kernel with handle caching (100 max)
514
- - `watch()` and `watchFile()` for file change notifications
515
- - `FileHandle` API (`fs.promises.open()`)
516
- - `Dir` API (`fs.promises.opendir()`)
517
- - `mkdtemp()` for temporary directories
518
- - `flush()` and `purge()` for cache management
519
- - Full `Dirent` support with `withFileTypes` option
488
+ **Complete architecture rewrite — VFS binary format with SharedArrayBuffer.**
489
+
490
+ **New Architecture:**
491
+ - VFS binary format all data stored in a single `.vfs.bin` file (Superblock → Inode Table → Path Table → Bitmap → Data Region)
492
+ - SharedArrayBuffer + Atomics for true zero-overhead synchronous operations
493
+ - Multi-tab leader/follower architecture with automatic failover via `navigator.locks` + Service Worker
494
+ - Bidirectional OPFS sync — VFS mutations mirrored to real OPFS files, external changes synced back via `FileSystemObserver`
495
+ - Workers inlined as blob URLs at build time (zero config, no external worker files)
496
+ - Echo suppression for OPFS sync (prevents infinite sync loops)
520
497
 
521
498
  **Performance:**
522
- - 2-3x faster git clone vs LightningFS
523
- - 1.6x faster reads
524
- - Handle caching eliminates repeated open/close overhead
525
- - Zero-copy data transfer with SharedArrayBuffer (Tier 1)
499
+ - 9-28x faster reads vs LightningFS
500
+ - 2.8-4x faster writes vs LightningFS
501
+ - 2.8-10x faster batch operations vs LightningFS
502
+ - Fire-and-forget OPFS sync zero impact on hot path
526
503
 
527
504
  **Breaking Changes:**
528
- - Requires `crossOriginIsolated` for Tier 1 (sync) features
529
- - New architecture - not backwards compatible with v1 internals
530
- - Minimum browser versions increased
505
+ - New API: `new VFSFileSystem(config)` instead of default `fs` singleton
506
+ - `createFS(config)` and `getDefaultFS()` helpers available
507
+ - Requires `crossOriginIsolated` for sync API (async API works everywhere)
508
+ - Complete internal rewrite — not backwards compatible with v2 internals
531
509
 
532
- ### v1.2.8 (2024)
510
+ ### v2.0.0 (2025)
533
511
 
534
- - Final release of v1 branch
535
- - OPFS-based async filesystem
536
- - Basic isomorphic-git compatibility
537
- - Cross-tab locking with `navigator.locks`
512
+ Major rewrite with sync API support via OPFS sync access handles and performance tiers.
538
513
 
539
514
  ### v1.0.0 (2024)
540
515
 
541
- - Initial release
542
- - Async-only OPFS filesystem
543
- - Node.js `fs.promises` compatible API
544
- - Basic directory and file operations
516
+ Initial release — async-only OPFS filesystem with `fs.promises` API.
545
517
 
546
518
  ## Contributing
547
519
 
@@ -549,9 +521,9 @@ Another tab or operation has the file open. The library uses `navigator.locks` t
549
521
  git clone https://github.com/componentor/fs
550
522
  cd fs
551
523
  npm install
552
- npm run dev # Watch mode
553
- npm test # Run tests
554
- npm run benchmark:open # Run benchmarks
524
+ npm run build # Build the library
525
+ npm test # Run unit tests (77 tests)
526
+ npm run benchmark:open # Run benchmarks in browser
555
527
  ```
556
528
 
557
529
  ## License