@componentor/fs 2.0.7 → 2.0.8
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 +3 -5
- package/dist/index.cjs +73 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +73 -119
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +52 -110
- package/dist/kernel.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -448,14 +448,12 @@ Another tab or operation has the file open. The library uses `navigator.locks` t
|
|
|
448
448
|
|
|
449
449
|
### v2.0.7 (2025)
|
|
450
450
|
|
|
451
|
-
**
|
|
451
|
+
**High-Performance Handle Caching with `readwrite-unsafe`:**
|
|
452
452
|
- Uses `readwrite-unsafe` mode (Chrome 121+) - no exclusive locks
|
|
453
|
+
- Zero per-operation overhead: cache lookup is a single Map.get()
|
|
453
454
|
- Browser extensions can access files while handles are cached
|
|
454
|
-
- Access-time based cleanup: handles released after 30s of inactivity
|
|
455
|
-
- Accessing a cached handle renews its timeout (stays cached during active use)
|
|
456
|
-
- Active operation protection: handles in use are never released
|
|
457
455
|
- LRU eviction when cache exceeds 100 handles
|
|
458
|
-
- Falls back to 100ms debounced release on older browsers
|
|
456
|
+
- Falls back to 100ms debounced release on older browsers (handles block)
|
|
459
457
|
|
|
460
458
|
### v2.0.2 (2025)
|
|
461
459
|
|
package/dist/index.cjs
CHANGED
|
@@ -434,111 +434,67 @@ const dirCache = new Map();
|
|
|
434
434
|
// Uses readwrite-unsafe mode when available (no exclusive lock, allows external access)
|
|
435
435
|
// Falls back to readwrite with debounced release for older browsers
|
|
436
436
|
const syncHandleCache = new Map();
|
|
437
|
-
const syncHandleLastAccess = new Map();
|
|
438
|
-
const syncHandleActiveOps = new Map(); // Track active operations per handle
|
|
439
437
|
const MAX_HANDLES = 100;
|
|
440
438
|
|
|
441
439
|
// Track if readwrite-unsafe mode is supported (detected on first use)
|
|
442
440
|
let unsafeModeSupported = null;
|
|
443
441
|
|
|
444
|
-
//
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const UNSAFE_IDLE_TIMEOUT = 30000;
|
|
449
|
-
const LEGACY_RELEASE_DELAY = 100;
|
|
450
|
-
|
|
451
|
-
// Track active operations to prevent releasing handles in use
|
|
452
|
-
function beginHandleOp(path) {
|
|
453
|
-
syncHandleActiveOps.set(path, (syncHandleActiveOps.get(path) || 0) + 1);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function endHandleOp(path) {
|
|
457
|
-
const count = syncHandleActiveOps.get(path) || 0;
|
|
458
|
-
if (count <= 1) {
|
|
459
|
-
syncHandleActiveOps.delete(path);
|
|
460
|
-
} else {
|
|
461
|
-
syncHandleActiveOps.set(path, count - 1);
|
|
462
|
-
}
|
|
442
|
+
// Debug tracing - set via 'setDebug' message
|
|
443
|
+
let debugTrace = false;
|
|
444
|
+
function trace(...args) {
|
|
445
|
+
if (debugTrace) console.log('[OPFS-T2]', ...args);
|
|
463
446
|
}
|
|
464
447
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
448
|
+
// Minimal timer for legacy mode only
|
|
449
|
+
let releaseTimer = null;
|
|
450
|
+
const LEGACY_RELEASE_DELAY = 100;
|
|
468
451
|
|
|
469
452
|
function scheduleHandleRelease() {
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
if (isHandleInUse(path)) continue;
|
|
482
|
-
if (now - lastAccess >= UNSAFE_IDLE_TIMEOUT) {
|
|
483
|
-
const h = syncHandleCache.get(path);
|
|
484
|
-
if (h) { try { h.flush(); h.close(); } catch {} }
|
|
485
|
-
syncHandleCache.delete(path);
|
|
486
|
-
syncHandleLastAccess.delete(path);
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
// Reschedule if there are still handles
|
|
490
|
-
if (syncHandleCache.size > 0) {
|
|
491
|
-
scheduleHandleRelease();
|
|
492
|
-
}
|
|
493
|
-
}, UNSAFE_IDLE_TIMEOUT);
|
|
494
|
-
} else {
|
|
495
|
-
// Legacy readwrite: release all handles after 100ms idle
|
|
496
|
-
releaseTimer = setTimeout(() => {
|
|
497
|
-
releaseTimer = null;
|
|
498
|
-
if (syncHandleCache.size > 0) {
|
|
499
|
-
for (const [path, h] of syncHandleCache) {
|
|
500
|
-
// Skip handles that are currently in use
|
|
501
|
-
if (isHandleInUse(path)) continue;
|
|
502
|
-
try { h.flush(); h.close(); } catch {}
|
|
503
|
-
syncHandleCache.delete(path);
|
|
504
|
-
syncHandleLastAccess.delete(path);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
}, LEGACY_RELEASE_DELAY);
|
|
508
|
-
}
|
|
453
|
+
if (unsafeModeSupported) return; // No release needed for readwrite-unsafe
|
|
454
|
+
if (releaseTimer) return; // Already scheduled
|
|
455
|
+
releaseTimer = setTimeout(() => {
|
|
456
|
+
releaseTimer = null;
|
|
457
|
+
const count = syncHandleCache.size;
|
|
458
|
+
for (const h of syncHandleCache.values()) {
|
|
459
|
+
try { h.flush(); h.close(); } catch {}
|
|
460
|
+
}
|
|
461
|
+
syncHandleCache.clear();
|
|
462
|
+
trace('Released ' + count + ' handles (legacy mode debounce)');
|
|
463
|
+
}, LEGACY_RELEASE_DELAY);
|
|
509
464
|
}
|
|
510
465
|
|
|
511
466
|
async function getSyncHandle(filePath, create) {
|
|
512
467
|
const cached = syncHandleCache.get(filePath);
|
|
513
468
|
if (cached) {
|
|
514
|
-
|
|
515
|
-
syncHandleLastAccess.set(filePath, Date.now());
|
|
469
|
+
trace('Handle cache HIT: ' + filePath);
|
|
516
470
|
return cached;
|
|
517
471
|
}
|
|
518
472
|
|
|
519
473
|
// Evict oldest handles if cache is full
|
|
520
474
|
if (syncHandleCache.size >= MAX_HANDLES) {
|
|
521
475
|
const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
|
|
476
|
+
trace('LRU evicting ' + keys.length + ' handles');
|
|
522
477
|
for (const key of keys) {
|
|
523
478
|
const h = syncHandleCache.get(key);
|
|
524
|
-
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key);
|
|
479
|
+
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
|
|
525
480
|
}
|
|
526
481
|
}
|
|
527
482
|
|
|
528
483
|
const fh = await getFileHandle(filePath, create);
|
|
529
484
|
|
|
530
485
|
// Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
|
|
531
|
-
// Falls back to readwrite if not supported
|
|
532
486
|
let access;
|
|
533
487
|
if (unsafeModeSupported === null) {
|
|
534
488
|
// First time - detect support
|
|
535
489
|
try {
|
|
536
490
|
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
537
491
|
unsafeModeSupported = true;
|
|
492
|
+
trace('readwrite-unsafe mode SUPPORTED - handles won\\'t block');
|
|
538
493
|
} catch {
|
|
539
494
|
// Not supported, use default mode
|
|
540
495
|
access = await fh.createSyncAccessHandle();
|
|
541
496
|
unsafeModeSupported = false;
|
|
497
|
+
trace('readwrite-unsafe mode NOT supported - using legacy mode');
|
|
542
498
|
}
|
|
543
499
|
} else if (unsafeModeSupported) {
|
|
544
500
|
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
@@ -547,13 +503,17 @@ async function getSyncHandle(filePath, create) {
|
|
|
547
503
|
}
|
|
548
504
|
|
|
549
505
|
syncHandleCache.set(filePath, access);
|
|
550
|
-
|
|
506
|
+
trace('Handle ACQUIRED: ' + filePath + ' (cache size: ' + syncHandleCache.size + ')');
|
|
551
507
|
return access;
|
|
552
508
|
}
|
|
553
509
|
|
|
554
510
|
function closeSyncHandle(filePath) {
|
|
555
511
|
const h = syncHandleCache.get(filePath);
|
|
556
|
-
if (h) {
|
|
512
|
+
if (h) {
|
|
513
|
+
try { h.close(); } catch {}
|
|
514
|
+
syncHandleCache.delete(filePath);
|
|
515
|
+
trace('Handle RELEASED: ' + filePath);
|
|
516
|
+
}
|
|
557
517
|
}
|
|
558
518
|
|
|
559
519
|
function closeHandlesUnder(prefix) {
|
|
@@ -561,7 +521,6 @@ function closeHandlesUnder(prefix) {
|
|
|
561
521
|
if (p === prefix || p.startsWith(prefix + '/')) {
|
|
562
522
|
try { h.close(); } catch {}
|
|
563
523
|
syncHandleCache.delete(p);
|
|
564
|
-
syncHandleLastAccess.delete(p);
|
|
565
524
|
}
|
|
566
525
|
}
|
|
567
526
|
}
|
|
@@ -636,17 +595,12 @@ async function getParentAndName(filePath) {
|
|
|
636
595
|
|
|
637
596
|
async function handleRead(filePath, payload) {
|
|
638
597
|
const access = await getSyncHandle(filePath, false);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
const bytesRead = access.read(buf, { at: offset });
|
|
646
|
-
return { data: buf.slice(0, bytesRead) };
|
|
647
|
-
} finally {
|
|
648
|
-
endHandleOp(filePath);
|
|
649
|
-
}
|
|
598
|
+
const size = access.getSize();
|
|
599
|
+
const offset = payload?.offset || 0;
|
|
600
|
+
const len = payload?.len || (size - offset);
|
|
601
|
+
const buf = new Uint8Array(len);
|
|
602
|
+
const bytesRead = access.read(buf, { at: offset });
|
|
603
|
+
return { data: buf.slice(0, bytesRead) };
|
|
650
604
|
}
|
|
651
605
|
|
|
652
606
|
// Non-blocking read using getFile() - does NOT lock the file
|
|
@@ -682,57 +636,39 @@ function handleReleaseHandle(filePath) {
|
|
|
682
636
|
|
|
683
637
|
// Force release ALL file handles - use before HMR notifications
|
|
684
638
|
function handleReleaseAllHandles() {
|
|
685
|
-
for (const
|
|
639
|
+
for (const h of syncHandleCache.values()) {
|
|
686
640
|
try { h.close(); } catch {}
|
|
687
641
|
}
|
|
688
642
|
syncHandleCache.clear();
|
|
689
|
-
syncHandleLastAccess.clear();
|
|
690
|
-
syncHandleActiveOps.clear();
|
|
691
643
|
return { success: true };
|
|
692
644
|
}
|
|
693
645
|
|
|
694
646
|
async function handleWrite(filePath, payload) {
|
|
695
647
|
const access = await getSyncHandle(filePath, true);
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
if (
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
access.write(payload.data, { at: offset });
|
|
702
|
-
// Only flush if explicitly requested (default: true for safety)
|
|
703
|
-
if (payload?.flush !== false) access.flush();
|
|
704
|
-
}
|
|
705
|
-
return { success: true };
|
|
706
|
-
} finally {
|
|
707
|
-
endHandleOp(filePath);
|
|
648
|
+
if (payload?.data) {
|
|
649
|
+
const offset = payload.offset ?? 0;
|
|
650
|
+
if (offset === 0) access.truncate(0);
|
|
651
|
+
access.write(payload.data, { at: offset });
|
|
652
|
+
if (payload?.flush !== false) access.flush();
|
|
708
653
|
}
|
|
654
|
+
return { success: true };
|
|
709
655
|
}
|
|
710
656
|
|
|
711
657
|
async function handleAppend(filePath, payload) {
|
|
712
658
|
const access = await getSyncHandle(filePath, true);
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
access.write(payload.data, { at: size });
|
|
718
|
-
if (payload?.flush !== false) access.flush();
|
|
719
|
-
}
|
|
720
|
-
return { success: true };
|
|
721
|
-
} finally {
|
|
722
|
-
endHandleOp(filePath);
|
|
659
|
+
if (payload?.data) {
|
|
660
|
+
const size = access.getSize();
|
|
661
|
+
access.write(payload.data, { at: size });
|
|
662
|
+
if (payload?.flush !== false) access.flush();
|
|
723
663
|
}
|
|
664
|
+
return { success: true };
|
|
724
665
|
}
|
|
725
666
|
|
|
726
667
|
async function handleTruncate(filePath, payload) {
|
|
727
668
|
const access = await getSyncHandle(filePath, false);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
access.flush();
|
|
732
|
-
return { success: true };
|
|
733
|
-
} finally {
|
|
734
|
-
endHandleOp(filePath);
|
|
735
|
-
}
|
|
669
|
+
access.truncate(payload?.len ?? 0);
|
|
670
|
+
access.flush();
|
|
671
|
+
return { success: true };
|
|
736
672
|
}
|
|
737
673
|
|
|
738
674
|
async function handleStat(filePath) {
|
|
@@ -901,18 +837,14 @@ function handleFlush() {
|
|
|
901
837
|
}
|
|
902
838
|
|
|
903
839
|
function handlePurge() {
|
|
904
|
-
// Cancel any pending release timer
|
|
905
840
|
if (releaseTimer) {
|
|
906
841
|
clearTimeout(releaseTimer);
|
|
907
842
|
releaseTimer = null;
|
|
908
843
|
}
|
|
909
|
-
|
|
910
|
-
for (const [, handle] of syncHandleCache) {
|
|
844
|
+
for (const handle of syncHandleCache.values()) {
|
|
911
845
|
try { handle.flush(); handle.close(); } catch {}
|
|
912
846
|
}
|
|
913
847
|
syncHandleCache.clear();
|
|
914
|
-
syncHandleLastAccess.clear();
|
|
915
|
-
syncHandleActiveOps.clear();
|
|
916
848
|
dirCache.clear();
|
|
917
849
|
cachedRoot = null;
|
|
918
850
|
return { success: true };
|
|
@@ -938,6 +870,10 @@ async function processMessage(msg) {
|
|
|
938
870
|
case 'purge': return handlePurge();
|
|
939
871
|
case 'releaseHandle': return handleReleaseHandle(path);
|
|
940
872
|
case 'releaseAllHandles': return handleReleaseAllHandles();
|
|
873
|
+
case 'setDebug':
|
|
874
|
+
debugTrace = !!payload?.enabled;
|
|
875
|
+
trace('Debug tracing ' + (debugTrace ? 'ENABLED' : 'DISABLED') + ', unsafeMode: ' + unsafeModeSupported);
|
|
876
|
+
return { success: true, debugTrace, unsafeModeSupported, cacheSize: syncHandleCache.size };
|
|
941
877
|
default: throw new Error('Unknown operation: ' + type);
|
|
942
878
|
}
|
|
943
879
|
}
|
|
@@ -1794,6 +1730,24 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
1794
1730
|
this.syncCall("purge", "/");
|
|
1795
1731
|
this.statCache.clear();
|
|
1796
1732
|
}
|
|
1733
|
+
/**
|
|
1734
|
+
* Enable or disable debug tracing for handle operations.
|
|
1735
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
1736
|
+
* @param enabled - Whether to enable debug tracing
|
|
1737
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
1738
|
+
*/
|
|
1739
|
+
setDebugSync(enabled) {
|
|
1740
|
+
return this.syncCall("setDebug", "/", { enabled });
|
|
1741
|
+
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Enable or disable debug tracing for handle operations (async version).
|
|
1744
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
1745
|
+
* @param enabled - Whether to enable debug tracing
|
|
1746
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
1747
|
+
*/
|
|
1748
|
+
async setDebug(enabled) {
|
|
1749
|
+
return this.asyncCall("setDebug", "/", { enabled });
|
|
1750
|
+
}
|
|
1797
1751
|
accessSync(filePath, _mode) {
|
|
1798
1752
|
const exists = this.existsSync(filePath);
|
|
1799
1753
|
if (!exists) {
|