@componentor/fs 2.0.2 → 2.0.5
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 +11 -1
- package/dist/index.cjs +108 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +108 -6
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +1 -1
- package/dist/kernel.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -446,10 +446,20 @@ Another tab or operation has the file open. The library uses `navigator.locks` t
|
|
|
446
446
|
|
|
447
447
|
## Changelog
|
|
448
448
|
|
|
449
|
+
### v2.0.4 (2025)
|
|
450
|
+
|
|
451
|
+
**Bug Fixes:**
|
|
452
|
+
- Handle idle timeout now works for both Tier 1 and Tier 2
|
|
453
|
+
- Previously only Tier 1 kernel had idle release; Tier 2 kernel now also releases handles after 2s
|
|
454
|
+
|
|
455
|
+
### v2.0.3 (2025)
|
|
456
|
+
- Reduced handle idle timeout from 5s to 2s for faster external tool access
|
|
457
|
+
- Added tests verifying handles are properly released after idle timeout
|
|
458
|
+
|
|
449
459
|
### v2.0.2 (2025)
|
|
450
460
|
|
|
451
461
|
**Improvements:**
|
|
452
|
-
- Sync access handles now auto-release after
|
|
462
|
+
- Sync access handles now auto-release after idle timeout
|
|
453
463
|
- Allows external tools (like OPFS Chrome extension) to access files when idle
|
|
454
464
|
- Maintains full performance during active operations
|
|
455
465
|
|
package/dist/index.cjs
CHANGED
|
@@ -431,31 +431,56 @@ let cachedRoot = null;
|
|
|
431
431
|
const dirCache = new Map();
|
|
432
432
|
|
|
433
433
|
// Sync handle cache - MAJOR performance optimization
|
|
434
|
+
// Handles auto-release after idle timeout to allow external tools to access files
|
|
434
435
|
const syncHandleCache = new Map();
|
|
436
|
+
const syncHandleLastAccess = new Map();
|
|
435
437
|
const MAX_HANDLES = 100;
|
|
438
|
+
const HANDLE_IDLE_TIMEOUT = 2000;
|
|
439
|
+
let idleCleanupTimer = null;
|
|
440
|
+
|
|
441
|
+
function scheduleIdleCleanup() {
|
|
442
|
+
if (idleCleanupTimer) return;
|
|
443
|
+
idleCleanupTimer = setTimeout(() => {
|
|
444
|
+
idleCleanupTimer = null;
|
|
445
|
+
const now = Date.now();
|
|
446
|
+
for (const [p, lastAccess] of syncHandleLastAccess) {
|
|
447
|
+
if (now - lastAccess > HANDLE_IDLE_TIMEOUT) {
|
|
448
|
+
const h = syncHandleCache.get(p);
|
|
449
|
+
if (h) { try { h.flush(); h.close(); } catch {} syncHandleCache.delete(p); }
|
|
450
|
+
syncHandleLastAccess.delete(p);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (syncHandleCache.size > 0) scheduleIdleCleanup();
|
|
454
|
+
}, HANDLE_IDLE_TIMEOUT);
|
|
455
|
+
}
|
|
436
456
|
|
|
437
457
|
async function getSyncHandle(filePath, create) {
|
|
438
458
|
const cached = syncHandleCache.get(filePath);
|
|
439
|
-
if (cached)
|
|
459
|
+
if (cached) {
|
|
460
|
+
syncHandleLastAccess.set(filePath, Date.now());
|
|
461
|
+
return cached;
|
|
462
|
+
}
|
|
440
463
|
|
|
441
464
|
// Evict oldest handles if cache is full
|
|
442
465
|
if (syncHandleCache.size >= MAX_HANDLES) {
|
|
443
466
|
const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
|
|
444
467
|
for (const key of keys) {
|
|
445
468
|
const h = syncHandleCache.get(key);
|
|
446
|
-
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
|
|
469
|
+
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); syncHandleLastAccess.delete(key); }
|
|
447
470
|
}
|
|
448
471
|
}
|
|
449
472
|
|
|
450
473
|
const fh = await getFileHandle(filePath, create);
|
|
451
474
|
const access = await fh.createSyncAccessHandle();
|
|
452
475
|
syncHandleCache.set(filePath, access);
|
|
476
|
+
syncHandleLastAccess.set(filePath, Date.now());
|
|
477
|
+
scheduleIdleCleanup();
|
|
453
478
|
return access;
|
|
454
479
|
}
|
|
455
480
|
|
|
456
481
|
function closeSyncHandle(filePath) {
|
|
457
482
|
const h = syncHandleCache.get(filePath);
|
|
458
|
-
if (h) { try { h.close(); } catch {} syncHandleCache.delete(filePath); }
|
|
483
|
+
if (h) { try { h.close(); } catch {} syncHandleCache.delete(filePath); syncHandleLastAccess.delete(filePath); }
|
|
459
484
|
}
|
|
460
485
|
|
|
461
486
|
function closeHandlesUnder(prefix) {
|
|
@@ -463,6 +488,7 @@ function closeHandlesUnder(prefix) {
|
|
|
463
488
|
if (p === prefix || p.startsWith(prefix + '/')) {
|
|
464
489
|
try { h.close(); } catch {}
|
|
465
490
|
syncHandleCache.delete(p);
|
|
491
|
+
syncHandleLastAccess.delete(p);
|
|
466
492
|
}
|
|
467
493
|
}
|
|
468
494
|
}
|
|
@@ -545,6 +571,47 @@ async function handleRead(filePath, payload) {
|
|
|
545
571
|
return { data: buf.slice(0, bytesRead) };
|
|
546
572
|
}
|
|
547
573
|
|
|
574
|
+
// Non-blocking read using getFile() - does NOT lock the file
|
|
575
|
+
// Use this for HMR scenarios where external tools need to modify files
|
|
576
|
+
async function handleReadAsync(filePath, payload) {
|
|
577
|
+
const parts = parsePath(filePath);
|
|
578
|
+
const fileName = parts.pop();
|
|
579
|
+
if (!fileName) throw new Error('Invalid file path');
|
|
580
|
+
const dir = parts.length > 0 ? await getDirectoryHandle(parts, false) : await getRoot();
|
|
581
|
+
const fh = await dir.getFileHandle(fileName);
|
|
582
|
+
const file = await fh.getFile();
|
|
583
|
+
|
|
584
|
+
const offset = payload?.offset || 0;
|
|
585
|
+
const len = payload?.len || (file.size - offset);
|
|
586
|
+
|
|
587
|
+
if (offset === 0 && len === file.size) {
|
|
588
|
+
// Fast path: read entire file
|
|
589
|
+
const buf = new Uint8Array(await file.arrayBuffer());
|
|
590
|
+
return { data: buf };
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Partial read using slice
|
|
594
|
+
const slice = file.slice(offset, offset + len);
|
|
595
|
+
const buf = new Uint8Array(await slice.arrayBuffer());
|
|
596
|
+
return { data: buf };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Force release a file handle - allows external tools to modify the file
|
|
600
|
+
function handleReleaseHandle(filePath) {
|
|
601
|
+
closeSyncHandle(filePath);
|
|
602
|
+
return { success: true };
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// Force release ALL file handles - use before HMR notifications
|
|
606
|
+
function handleReleaseAllHandles() {
|
|
607
|
+
for (const [p, h] of syncHandleCache) {
|
|
608
|
+
try { h.close(); } catch {}
|
|
609
|
+
}
|
|
610
|
+
syncHandleCache.clear();
|
|
611
|
+
syncHandleLastAccess.clear();
|
|
612
|
+
return { success: true };
|
|
613
|
+
}
|
|
614
|
+
|
|
548
615
|
async function handleWrite(filePath, payload) {
|
|
549
616
|
const access = await getSyncHandle(filePath, true);
|
|
550
617
|
if (payload?.data) {
|
|
@@ -754,6 +821,7 @@ async function processMessage(msg) {
|
|
|
754
821
|
const { type, path, payload } = msg;
|
|
755
822
|
switch (type) {
|
|
756
823
|
case 'read': return handleRead(path, payload);
|
|
824
|
+
case 'readAsync': return handleReadAsync(path, payload);
|
|
757
825
|
case 'write': return handleWrite(path, payload);
|
|
758
826
|
case 'append': return handleAppend(path, payload);
|
|
759
827
|
case 'truncate': return handleTruncate(path, payload);
|
|
@@ -767,6 +835,8 @@ async function processMessage(msg) {
|
|
|
767
835
|
case 'copy': return handleCopy(path, payload);
|
|
768
836
|
case 'flush': return handleFlush();
|
|
769
837
|
case 'purge': return handlePurge();
|
|
838
|
+
case 'releaseHandle': return handleReleaseHandle(path);
|
|
839
|
+
case 'releaseAllHandles': return handleReleaseAllHandles();
|
|
770
840
|
default: throw new Error('Unknown operation: ' + type);
|
|
771
841
|
}
|
|
772
842
|
}
|
|
@@ -2007,6 +2077,22 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
2007
2077
|
await this.fastCall("purge", "/");
|
|
2008
2078
|
this.statCache.clear();
|
|
2009
2079
|
}
|
|
2080
|
+
/**
|
|
2081
|
+
* Release all cached file handles.
|
|
2082
|
+
* Call this before expecting external tools (OPFS Explorer, browser console, etc.)
|
|
2083
|
+
* to modify files. This allows external access without waiting for the idle timeout.
|
|
2084
|
+
* Unlike purge(), this only releases file handles without clearing directory caches.
|
|
2085
|
+
*/
|
|
2086
|
+
async releaseAllHandles() {
|
|
2087
|
+
await this.fastCall("releaseAllHandles", "/");
|
|
2088
|
+
}
|
|
2089
|
+
/**
|
|
2090
|
+
* Release a specific file's handle.
|
|
2091
|
+
* Use this when you know a specific file needs to be externally modified.
|
|
2092
|
+
*/
|
|
2093
|
+
async releaseHandle(filePath) {
|
|
2094
|
+
await this.fastCall("releaseHandle", filePath);
|
|
2095
|
+
}
|
|
2010
2096
|
// Constants
|
|
2011
2097
|
constants = constants;
|
|
2012
2098
|
// --- FileHandle Implementation ---
|
|
@@ -2332,8 +2418,9 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
2332
2418
|
const self2 = this;
|
|
2333
2419
|
let observer = null;
|
|
2334
2420
|
let closed = false;
|
|
2335
|
-
const callback = (records) => {
|
|
2421
|
+
const callback = async (records) => {
|
|
2336
2422
|
if (closed) return;
|
|
2423
|
+
await self2.releaseAllHandles();
|
|
2337
2424
|
for (const record of records) {
|
|
2338
2425
|
if (record.type === "errored" || record.type === "unknown") continue;
|
|
2339
2426
|
const filename = record.relativePathComponents.length > 0 ? record.relativePathComponents[record.relativePathComponents.length - 1] : basename(absPath);
|
|
@@ -2373,26 +2460,38 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
2373
2460
|
const entries = await this.promises.readdir(absPath);
|
|
2374
2461
|
const currentEntries = new Set(entries);
|
|
2375
2462
|
if (lastEntries !== null) {
|
|
2463
|
+
let hasChanges = false;
|
|
2464
|
+
const added = [];
|
|
2465
|
+
const removed = [];
|
|
2376
2466
|
for (const entry of currentEntries) {
|
|
2377
2467
|
if (!lastEntries.has(entry)) {
|
|
2378
|
-
|
|
2468
|
+
added.push(entry);
|
|
2469
|
+
hasChanges = true;
|
|
2379
2470
|
}
|
|
2380
2471
|
}
|
|
2381
2472
|
for (const entry of lastEntries) {
|
|
2382
2473
|
if (!currentEntries.has(entry)) {
|
|
2383
|
-
|
|
2474
|
+
removed.push(entry);
|
|
2475
|
+
hasChanges = true;
|
|
2384
2476
|
}
|
|
2385
2477
|
}
|
|
2478
|
+
if (hasChanges) {
|
|
2479
|
+
await this.releaseAllHandles();
|
|
2480
|
+
for (const entry of added) cb?.("rename", entry);
|
|
2481
|
+
for (const entry of removed) cb?.("rename", entry);
|
|
2482
|
+
}
|
|
2386
2483
|
}
|
|
2387
2484
|
lastEntries = currentEntries;
|
|
2388
2485
|
} else {
|
|
2389
2486
|
if (lastMtimeMs !== null && stat.mtimeMs !== lastMtimeMs) {
|
|
2487
|
+
await this.releaseAllHandles();
|
|
2390
2488
|
cb?.("change", basename(absPath));
|
|
2391
2489
|
}
|
|
2392
2490
|
lastMtimeMs = stat.mtimeMs;
|
|
2393
2491
|
}
|
|
2394
2492
|
} catch {
|
|
2395
2493
|
if (lastMtimeMs !== null || lastEntries !== null) {
|
|
2494
|
+
await this.releaseAllHandles();
|
|
2396
2495
|
cb?.("rename", basename(absPath));
|
|
2397
2496
|
lastMtimeMs = null;
|
|
2398
2497
|
lastEntries = null;
|
|
@@ -2426,6 +2525,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
2426
2525
|
const stat = await this.promises.stat(absPath);
|
|
2427
2526
|
if (lastStat !== null) {
|
|
2428
2527
|
if (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size) {
|
|
2528
|
+
await this.releaseAllHandles();
|
|
2429
2529
|
cb?.(stat, lastStat);
|
|
2430
2530
|
}
|
|
2431
2531
|
}
|
|
@@ -2433,6 +2533,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
2433
2533
|
} catch {
|
|
2434
2534
|
const emptyStat = createStats({ type: "file", size: 0, mtimeMs: 0, mode: 0 });
|
|
2435
2535
|
if (lastStat !== null) {
|
|
2536
|
+
await this.releaseAllHandles();
|
|
2436
2537
|
cb?.(emptyStat, lastStat);
|
|
2437
2538
|
}
|
|
2438
2539
|
lastStat = emptyStat;
|
|
@@ -2441,6 +2542,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
2441
2542
|
if (_OPFSFileSystem.hasNativeObserver && cb) {
|
|
2442
2543
|
const self2 = this;
|
|
2443
2544
|
const observerCallback = async () => {
|
|
2545
|
+
await self2.releaseAllHandles();
|
|
2444
2546
|
try {
|
|
2445
2547
|
const stat = await self2.promises.stat(absPath);
|
|
2446
2548
|
if (lastStat !== null && (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size)) {
|