@componentor/fs 3.0.17 → 3.0.19
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 +51 -1
- package/dist/index.d.mts +16 -1
- package/dist/index.js +46 -3
- package/dist/index.js.map +1 -1
- package/dist/workers/repair.worker.js +38 -1
- package/dist/workers/repair.worker.js.map +1 -1
- package/dist/workers/server.worker.js +38 -1
- package/dist/workers/server.worker.js.map +1 -1
- package/dist/workers/sync-relay.worker.js +109 -3
- package/dist/workers/sync-relay.worker.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,7 +82,15 @@ const fs = new VFSFileSystem({
|
|
|
82
82
|
strictPermissions: false, // Enforce Unix permissions (default: false)
|
|
83
83
|
sabSize: 4194304, // SharedArrayBuffer size in bytes (default: 4MB)
|
|
84
84
|
debug: false, // Enable debug logging (default: false)
|
|
85
|
+
swUrl: undefined, // URL of the service worker script (default: auto-resolved)
|
|
85
86
|
swScope: undefined, // Custom service worker scope (default: auto-scoped per root)
|
|
87
|
+
limits: { // Upper bounds for VFS validation (prevents corrupt data from causing OOM)
|
|
88
|
+
maxInodes: 4_000_000, // Max inode count (default: 4M)
|
|
89
|
+
maxBlocks: 4_000_000, // Max data blocks (default: 4M)
|
|
90
|
+
maxPathTable: 256 * 1024 * 1024, // Max path table bytes (default: 256MB)
|
|
91
|
+
maxVFSSize: 100 * 1024 * 1024 * 1024, // Max .vfs.bin size (default: 100GB)
|
|
92
|
+
maxPayload: 2 * 1024 * 1024 * 1024, // Max single SAB payload (default: 2GB)
|
|
93
|
+
},
|
|
86
94
|
});
|
|
87
95
|
```
|
|
88
96
|
|
|
@@ -153,6 +161,30 @@ console.log(fs.mode); // 'hybrid'
|
|
|
153
161
|
|
|
154
162
|
`setMode()` terminates internal workers, allocates fresh shared memory, and reinitializes the filesystem in the requested mode.
|
|
155
163
|
|
|
164
|
+
### Service Worker Setup (Multi-Tab)
|
|
165
|
+
|
|
166
|
+
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**.
|
|
167
|
+
|
|
168
|
+
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:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
const fs = new VFSFileSystem({
|
|
172
|
+
swUrl: '/vfs-service-worker.js', // your public URL
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Vite example** — copy the file to `public/`:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
cp node_modules/@componentor/fs/dist/workers/service.worker.js public/vfs-service-worker.js
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const fs = new VFSFileSystem({ swUrl: '/vfs-service-worker.js' });
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
If you only use a single tab, the service worker is not needed — the tab always runs as the leader.
|
|
187
|
+
|
|
156
188
|
## COOP/COEP Headers
|
|
157
189
|
|
|
158
190
|
To enable the sync API, your page must be `crossOriginIsolated`. Add these headers:
|
|
@@ -570,6 +602,24 @@ Make sure `opfsSync` is enabled (it's `true` by default). Files are mirrored to
|
|
|
570
602
|
|
|
571
603
|
## Changelog
|
|
572
604
|
|
|
605
|
+
### v3.0.19 (2026)
|
|
606
|
+
|
|
607
|
+
**Features:**
|
|
608
|
+
- Add `swUrl` config option to specify a custom service worker URL for multi-tab support in bundled environments where the default auto-resolved URL doesn't work
|
|
609
|
+
- Remove `type: 'module'` from service worker registration (built output is plain script, not ESM)
|
|
610
|
+
|
|
611
|
+
### v3.0.18 (2026)
|
|
612
|
+
|
|
613
|
+
**Features:**
|
|
614
|
+
- Configurable VFS limits via `limits` option: `maxInodes`, `maxBlocks`, `maxPathTable`, `maxVFSSize`, `maxPayload`
|
|
615
|
+
|
|
616
|
+
**Fixes:**
|
|
617
|
+
- Pre-validate superblock before `engine.init()` to prevent hangs from corrupt values causing huge allocations
|
|
618
|
+
- Add upper bounds in `mount()`: max 4M inodes, 4M blocks, 256MB path table, 100GB total VFS size
|
|
619
|
+
- Ensure all mount errors are prefixed with `Corrupt VFS:` for consistent corruption fallback
|
|
620
|
+
- Cap `readPayload()` at 2GB (configurable) and validate each chunk length in the multi-chunk loop to prevent OOM/infinite loops from corrupt SAB data
|
|
621
|
+
- Cap `MemoryHandle.grow()` at 4GB to prevent OOM from corrupt VFS offsets on main-thread fallback
|
|
622
|
+
|
|
573
623
|
### v3.0.17 (2026)
|
|
574
624
|
|
|
575
625
|
**Features:**
|
|
@@ -757,7 +807,7 @@ git clone https://github.com/componentor/fs
|
|
|
757
807
|
cd fs
|
|
758
808
|
npm install
|
|
759
809
|
npm run build # Build the library
|
|
760
|
-
npm test # Run unit tests (
|
|
810
|
+
npm test # Run unit tests (107 tests)
|
|
761
811
|
npm run benchmark:open # Run benchmarks in browser
|
|
762
812
|
```
|
|
763
813
|
|
package/dist/index.d.mts
CHANGED
|
@@ -140,6 +140,19 @@ type WatchFileListener = (curr: Stats, prev: Stats) => void;
|
|
|
140
140
|
* Automatically selected as fallback when VFS corruption is detected in hybrid mode.
|
|
141
141
|
*/
|
|
142
142
|
type FSMode = 'hybrid' | 'vfs' | 'opfs';
|
|
143
|
+
/** Upper bounds for VFS validation. Prevents corrupt data from causing OOM/hangs. */
|
|
144
|
+
interface VFSLimits {
|
|
145
|
+
/** Maximum number of inodes (default: 4,000,000) */
|
|
146
|
+
maxInodes?: number;
|
|
147
|
+
/** Maximum number of data blocks (default: 4,000,000) */
|
|
148
|
+
maxBlocks?: number;
|
|
149
|
+
/** Maximum path table size in bytes (default: 256MB) */
|
|
150
|
+
maxPathTable?: number;
|
|
151
|
+
/** Maximum total VFS file size in bytes (default: 100GB) */
|
|
152
|
+
maxVFSSize?: number;
|
|
153
|
+
/** Maximum single SAB payload size in bytes (default: 2GB) */
|
|
154
|
+
maxPayload?: number;
|
|
155
|
+
}
|
|
143
156
|
/** VFS configuration options */
|
|
144
157
|
interface VFSConfig {
|
|
145
158
|
root?: string;
|
|
@@ -158,6 +171,8 @@ interface VFSConfig {
|
|
|
158
171
|
* `'./opfs-fs-sw/'` (relative to the SW script URL) so it won't collide
|
|
159
172
|
* with the host application's service worker. */
|
|
160
173
|
swScope?: string;
|
|
174
|
+
/** Upper bounds for VFS validation (prevents corrupt data from causing OOM/hangs). */
|
|
175
|
+
limits?: VFSLimits;
|
|
161
176
|
}
|
|
162
177
|
|
|
163
178
|
type AsyncRequestFn = (op: number, path: string, flags?: number, data?: Uint8Array | string | null, path2?: string, fdArgs?: Record<string, unknown>) => Promise<{
|
|
@@ -506,4 +521,4 @@ declare function getDefaultFS(): VFSFileSystem;
|
|
|
506
521
|
/** Async init helper — avoids blocking main thread */
|
|
507
522
|
declare function init(): Promise<void>;
|
|
508
523
|
|
|
509
|
-
export { type Dir, type Dirent, type Encoding, FSError, type FSMode, type FSWatcher, type FileHandle, type LoadResult, type MkdirOptions, type PathLike, type ReadOptions, type ReadStreamOptions, type ReaddirOptions, type RepairResult, type RmOptions, type RmdirOptions, type Stats, type UnpackResult, type VFSConfig, VFSFileSystem, type WatchEventType, type WatchFileListener, type WatchListener, type WatchOptions, type WriteOptions, type WriteStreamOptions, constants, createError, createFS, getDefaultFS, init, loadFromOPFS, path, repairVFS, statusToError, unpackToOPFS };
|
|
524
|
+
export { type Dir, type Dirent, type Encoding, FSError, type FSMode, type FSWatcher, type FileHandle, type LoadResult, type MkdirOptions, type PathLike, type ReadOptions, type ReadStreamOptions, type ReaddirOptions, type RepairResult, type RmOptions, type RmdirOptions, type Stats, type UnpackResult, type VFSConfig, VFSFileSystem, type VFSLimits, type WatchEventType, type WatchFileListener, type WatchListener, type WatchOptions, type WriteOptions, type WriteStreamOptions, constants, createError, createFS, getDefaultFS, init, loadFromOPFS, path, repairVFS, statusToError, unpackToOPFS };
|
package/dist/index.js
CHANGED
|
@@ -1351,7 +1351,8 @@ var VFSFileSystem = class {
|
|
|
1351
1351
|
strictPermissions: config.strictPermissions ?? false,
|
|
1352
1352
|
sabSize: config.sabSize ?? DEFAULT_SAB_SIZE,
|
|
1353
1353
|
debug: config.debug ?? false,
|
|
1354
|
-
swScope: config.swScope
|
|
1354
|
+
swScope: config.swScope,
|
|
1355
|
+
limits: config.limits
|
|
1355
1356
|
};
|
|
1356
1357
|
this.tabId = crypto.randomUUID();
|
|
1357
1358
|
this.ns = ns;
|
|
@@ -1473,7 +1474,8 @@ var VFSFileSystem = class {
|
|
|
1473
1474
|
gid: this.config.gid,
|
|
1474
1475
|
umask: this.config.umask,
|
|
1475
1476
|
strictPermissions: this.config.strictPermissions,
|
|
1476
|
-
debug: this.config.debug
|
|
1477
|
+
debug: this.config.debug,
|
|
1478
|
+
limits: this.config.limits
|
|
1477
1479
|
}
|
|
1478
1480
|
});
|
|
1479
1481
|
}
|
|
@@ -2193,6 +2195,13 @@ var VFSEngine = class {
|
|
|
2193
2195
|
superblockDirty = false;
|
|
2194
2196
|
// Free inode hint — skip O(n) scan
|
|
2195
2197
|
freeInodeHint = 0;
|
|
2198
|
+
// Configurable upper bounds
|
|
2199
|
+
maxInodes = 4e6;
|
|
2200
|
+
maxBlocks = 4e6;
|
|
2201
|
+
maxPathTable = 256 * 1024 * 1024;
|
|
2202
|
+
// 256MB
|
|
2203
|
+
maxVFSSize = 100 * 1024 * 1024 * 1024;
|
|
2204
|
+
// 100GB
|
|
2196
2205
|
init(handle, opts) {
|
|
2197
2206
|
this.handle = handle;
|
|
2198
2207
|
this.processUid = opts?.uid ?? 0;
|
|
@@ -2200,11 +2209,23 @@ var VFSEngine = class {
|
|
|
2200
2209
|
this.umask = opts?.umask ?? DEFAULT_UMASK;
|
|
2201
2210
|
this.strictPermissions = opts?.strictPermissions ?? false;
|
|
2202
2211
|
this.debug = opts?.debug ?? false;
|
|
2212
|
+
if (opts?.limits) {
|
|
2213
|
+
if (opts.limits.maxInodes != null) this.maxInodes = opts.limits.maxInodes;
|
|
2214
|
+
if (opts.limits.maxBlocks != null) this.maxBlocks = opts.limits.maxBlocks;
|
|
2215
|
+
if (opts.limits.maxPathTable != null) this.maxPathTable = opts.limits.maxPathTable;
|
|
2216
|
+
if (opts.limits.maxVFSSize != null) this.maxVFSSize = opts.limits.maxVFSSize;
|
|
2217
|
+
}
|
|
2203
2218
|
const size = handle.getSize();
|
|
2204
2219
|
if (size === 0) {
|
|
2205
2220
|
this.format();
|
|
2206
2221
|
} else {
|
|
2207
|
-
|
|
2222
|
+
try {
|
|
2223
|
+
this.mount();
|
|
2224
|
+
} catch (err) {
|
|
2225
|
+
const msg = err.message ?? String(err);
|
|
2226
|
+
if (msg.startsWith("Corrupt VFS:")) throw err;
|
|
2227
|
+
throw new Error(`Corrupt VFS: ${msg}`);
|
|
2228
|
+
}
|
|
2208
2229
|
}
|
|
2209
2230
|
}
|
|
2210
2231
|
/** Release the sync access handle (call on fatal error or shutdown) */
|
|
@@ -2271,6 +2292,18 @@ var VFSEngine = class {
|
|
|
2271
2292
|
if (freeBlocks > totalBlocks) {
|
|
2272
2293
|
throw new Error(`Corrupt VFS: free blocks (${freeBlocks}) exceeds total blocks (${totalBlocks})`);
|
|
2273
2294
|
}
|
|
2295
|
+
if (inodeCount > this.maxInodes) {
|
|
2296
|
+
throw new Error(`Corrupt VFS: inode count ${inodeCount} exceeds maximum ${this.maxInodes}`);
|
|
2297
|
+
}
|
|
2298
|
+
if (totalBlocks > this.maxBlocks) {
|
|
2299
|
+
throw new Error(`Corrupt VFS: total blocks ${totalBlocks} exceeds maximum ${this.maxBlocks}`);
|
|
2300
|
+
}
|
|
2301
|
+
if (fileSize > this.maxVFSSize) {
|
|
2302
|
+
throw new Error(`Corrupt VFS: file size ${fileSize} exceeds maximum ${this.maxVFSSize}`);
|
|
2303
|
+
}
|
|
2304
|
+
if (!Number.isFinite(inodeTableOffset) || inodeTableOffset < 0 || !Number.isFinite(pathTableOffset) || pathTableOffset < 0 || !Number.isFinite(bitmapOffset) || bitmapOffset < 0 || !Number.isFinite(dataOffset) || dataOffset < 0) {
|
|
2305
|
+
throw new Error(`Corrupt VFS: non-finite or negative section offset`);
|
|
2306
|
+
}
|
|
2274
2307
|
if (inodeTableOffset !== SUPERBLOCK.SIZE) {
|
|
2275
2308
|
throw new Error(`Corrupt VFS: inode table offset ${inodeTableOffset} (expected ${SUPERBLOCK.SIZE})`);
|
|
2276
2309
|
}
|
|
@@ -2288,7 +2321,13 @@ var VFSEngine = class {
|
|
|
2288
2321
|
if (pathUsed > pathTableSize) {
|
|
2289
2322
|
throw new Error(`Corrupt VFS: path used (${pathUsed}) exceeds path table size (${pathTableSize})`);
|
|
2290
2323
|
}
|
|
2324
|
+
if (pathTableSize > this.maxPathTable) {
|
|
2325
|
+
throw new Error(`Corrupt VFS: path table size ${pathTableSize} exceeds maximum ${this.maxPathTable}`);
|
|
2326
|
+
}
|
|
2291
2327
|
const expectedMinSize = dataOffset + totalBlocks * blockSize;
|
|
2328
|
+
if (expectedMinSize > this.maxVFSSize) {
|
|
2329
|
+
throw new Error(`Corrupt VFS: computed layout size ${expectedMinSize} exceeds maximum ${this.maxVFSSize}`);
|
|
2330
|
+
}
|
|
2292
2331
|
if (fileSize < expectedMinSize) {
|
|
2293
2332
|
throw new Error(`Corrupt VFS: file size ${fileSize} too small for layout (need ${expectedMinSize})`);
|
|
2294
2333
|
}
|
|
@@ -3483,6 +3522,10 @@ var MemoryHandle = class {
|
|
|
3483
3522
|
return this.buf.buffer.slice(0, this.len);
|
|
3484
3523
|
}
|
|
3485
3524
|
grow(minSize) {
|
|
3525
|
+
const MAX_SIZE = 4 * 1024 * 1024 * 1024;
|
|
3526
|
+
if (minSize > MAX_SIZE) {
|
|
3527
|
+
throw new Error(`MemoryHandle: cannot grow to ${minSize} bytes (max ${MAX_SIZE})`);
|
|
3528
|
+
}
|
|
3486
3529
|
const newSize = Math.max(minSize, this.buf.length * 2);
|
|
3487
3530
|
const newBuf = new Uint8Array(newSize);
|
|
3488
3531
|
newBuf.set(this.buf.subarray(0, this.len));
|