@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 +266 -294
- package/dist/index.js +1453 -2565
- package/dist/index.js.map +1 -1
- package/dist/workers/async-relay.worker.js +298 -0
- package/dist/workers/async-relay.worker.js.map +1 -0
- package/dist/workers/opfs-sync.worker.js +249 -0
- package/dist/workers/opfs-sync.worker.js.map +1 -0
- package/dist/workers/server.worker.js +1547 -0
- package/dist/workers/server.worker.js.map +1 -0
- package/dist/workers/service.worker.js +39 -0
- package/dist/workers/service.worker.js.map +1 -0
- package/dist/workers/sync-relay.worker.js +2031 -0
- package/dist/workers/sync-relay.worker.js.map +1 -0
- package/package.json +11 -11
- package/dist/index.cjs +0 -2811
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -641
- package/dist/index.d.ts +0 -641
- package/dist/kernel.js +0 -544
- package/dist/kernel.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
# @componentor/fs
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**High-performance OPFS-based Node.js `fs` polyfill for the browser**
|
|
4
4
|
|
|
5
|
-
A
|
|
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 {
|
|
8
|
+
import { VFSFileSystem } from '@componentor/fs';
|
|
9
9
|
|
|
10
|
-
|
|
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
|
-
- **
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
- **
|
|
26
|
-
- **
|
|
27
|
-
- **
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
51
|
-
|
|
52
|
-
console.log(
|
|
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
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
57
|
+
### Convenience Helpers
|
|
65
58
|
|
|
66
|
-
|
|
59
|
+
```typescript
|
|
60
|
+
import { createFS, getDefaultFS, init } from '@componentor/fs';
|
|
67
61
|
|
|
68
|
-
|
|
62
|
+
// Create with config
|
|
63
|
+
const fs = createFS({ root: '/repo', debug: true });
|
|
69
64
|
|
|
70
|
-
|
|
65
|
+
// Lazy singleton (created on first access)
|
|
66
|
+
const defaultFs = getDefaultFS();
|
|
71
67
|
|
|
72
|
-
|
|
68
|
+
// Async init helper
|
|
69
|
+
await init(); // initializes the default singleton
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Configuration
|
|
73
73
|
|
|
74
74
|
```typescript
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
107
|
+
## COOP/COEP Headers
|
|
95
108
|
|
|
96
|
-
To enable
|
|
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
|
-
|
|
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
|
|
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
|
|
158
|
+
### Runtime Check
|
|
145
159
|
|
|
146
160
|
```typescript
|
|
147
161
|
if (crossOriginIsolated) {
|
|
148
|
-
|
|
162
|
+
// Sync + async APIs available
|
|
149
163
|
fs.writeFileSync('/fast.txt', 'blazing fast');
|
|
150
164
|
} else {
|
|
151
|
-
|
|
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
|
|
172
|
+
Tested against LightningFS (IndexedDB-based) in Chrome with `crossOriginIsolated` enabled:
|
|
159
173
|
|
|
160
|
-
| Operation |
|
|
161
|
-
|
|
162
|
-
| Write 100 x 1KB |
|
|
163
|
-
| Write 100 x 4KB |
|
|
164
|
-
| Read 100 x 1KB |
|
|
165
|
-
| Read 100 x 4KB |
|
|
166
|
-
| Large 10 x 1MB |
|
|
167
|
-
| Batch Write 500
|
|
168
|
-
| Batch Read 500
|
|
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
|
-
- **
|
|
174
|
-
- **
|
|
175
|
-
- **
|
|
176
|
-
-
|
|
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 (
|
|
198
|
+
### Sync API (requires crossOriginIsolated)
|
|
189
199
|
|
|
190
200
|
```typescript
|
|
191
201
|
// Read/Write
|
|
192
|
-
fs.readFileSync(path
|
|
193
|
-
fs.writeFileSync(path
|
|
194
|
-
fs.appendFileSync(path
|
|
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
|
|
198
|
-
fs.rmdirSync(path
|
|
199
|
-
fs.
|
|
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
|
|
203
|
-
fs.renameSync(oldPath
|
|
204
|
-
fs.copyFileSync(src
|
|
205
|
-
fs.truncateSync(path
|
|
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
|
|
209
|
-
fs.
|
|
210
|
-
fs.
|
|
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 (
|
|
247
|
+
### Async API (always available)
|
|
214
248
|
|
|
215
249
|
```typescript
|
|
216
250
|
// Read/Write
|
|
217
|
-
fs.promises.readFile(path
|
|
218
|
-
fs.promises.writeFile(path
|
|
219
|
-
fs.promises.appendFile(path
|
|
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
|
|
223
|
-
fs.promises.rmdir(path
|
|
224
|
-
fs.promises.
|
|
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
|
|
228
|
-
fs.promises.rename(oldPath
|
|
229
|
-
fs.promises.copyFile(src
|
|
230
|
-
fs.promises.truncate(path
|
|
231
|
-
fs.promises.
|
|
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
|
|
235
|
-
fs.promises.lstat(path
|
|
236
|
-
fs.promises.exists(path
|
|
237
|
-
fs.promises.access(path
|
|
238
|
-
fs.promises.realpath(path
|
|
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
|
|
242
|
-
fs.promises.opendir(path
|
|
243
|
-
fs.promises.mkdtemp(prefix
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
fs.promises.
|
|
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
|
-
//
|
|
257
|
-
fs.createReadStream(
|
|
258
|
-
start
|
|
259
|
-
end
|
|
260
|
-
highWaterMark
|
|
261
|
-
})
|
|
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
|
-
//
|
|
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')
|
|
288
|
-
path.resolve('foo', 'bar')
|
|
289
|
-
path.dirname('/foo/bar/baz.txt')
|
|
290
|
-
path.basename('/foo/bar/baz.txt')
|
|
291
|
-
path.extname('/foo/bar/baz.txt')
|
|
292
|
-
path.normalize('/foo//bar/../baz')
|
|
293
|
-
path.isAbsolute('/foo')
|
|
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')
|
|
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 {
|
|
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
|
-
│
|
|
356
|
-
│
|
|
357
|
-
│ │ Sync API
|
|
358
|
-
│ │ readFileSync│ │ promises.
|
|
359
|
-
│ │writeFileSync│ │ readFile
|
|
360
|
-
│
|
|
361
|
-
│ │
|
|
362
|
-
│
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
│
|
|
369
|
-
│
|
|
370
|
-
│ │
|
|
371
|
-
│ │
|
|
372
|
-
│ │ │
|
|
373
|
-
│ │
|
|
374
|
-
│
|
|
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
|
-
│
|
|
383
|
-
│
|
|
384
|
-
│
|
|
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 |
|
|
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 `
|
|
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
|
|
458
|
+
Your page is not `crossOriginIsolated`. Add COOP/COEP headers (see above). The async API still works without them.
|
|
455
459
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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`
|
|
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
|
-
###
|
|
468
|
+
### Files not visible in OPFS DevTools
|
|
466
469
|
|
|
467
|
-
|
|
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
|
-
###
|
|
472
|
+
### External OPFS changes not detected
|
|
470
473
|
|
|
471
|
-
|
|
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
|
-
###
|
|
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
|
-
-
|
|
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
|
-
|
|
483
|
+
**Benchmark:**
|
|
484
|
+
- Add memfs (in-memory) to the benchmark suite for comparison
|
|
507
485
|
|
|
508
|
-
|
|
486
|
+
### v3.0.0 (2026)
|
|
509
487
|
|
|
510
|
-
**
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
-
|
|
514
|
-
-
|
|
515
|
-
-
|
|
516
|
-
-
|
|
517
|
-
-
|
|
518
|
-
-
|
|
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
|
-
-
|
|
523
|
-
-
|
|
524
|
-
-
|
|
525
|
-
-
|
|
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
|
-
-
|
|
529
|
-
-
|
|
530
|
-
-
|
|
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
|
-
###
|
|
510
|
+
### v2.0.0 (2025)
|
|
533
511
|
|
|
534
|
-
|
|
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
|
-
-
|
|
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
|
|
553
|
-
npm test
|
|
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
|