@componentor/fs 2.0.6 → 2.0.7
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 +10 -14
- package/dist/index.cjs +145 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +145 -41
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +119 -46
- package/dist/kernel.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -446,20 +446,16 @@ Another tab or operation has the file open. The library uses `navigator.locks` t
|
|
|
446
446
|
|
|
447
447
|
## Changelog
|
|
448
448
|
|
|
449
|
-
### v2.0.
|
|
450
|
-
|
|
451
|
-
**
|
|
452
|
-
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
-
|
|
458
|
-
-
|
|
459
|
-
|
|
460
|
-
### v2.0.3 (2025)
|
|
461
|
-
- Reduced handle idle timeout from 5s to 2s for faster external tool access
|
|
462
|
-
- Added tests verifying handles are properly released after idle timeout
|
|
449
|
+
### v2.0.7 (2025)
|
|
450
|
+
|
|
451
|
+
**Smart Handle Caching with `readwrite-unsafe`:**
|
|
452
|
+
- Uses `readwrite-unsafe` mode (Chrome 121+) - no exclusive locks
|
|
453
|
+
- 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
|
+
- LRU eviction when cache exceeds 100 handles
|
|
458
|
+
- Falls back to 100ms debounced release on older browsers
|
|
463
459
|
|
|
464
460
|
### v2.0.2 (2025)
|
|
465
461
|
|
package/dist/index.cjs
CHANGED
|
@@ -430,33 +430,88 @@ let isReady = false;
|
|
|
430
430
|
let cachedRoot = null;
|
|
431
431
|
const dirCache = new Map();
|
|
432
432
|
|
|
433
|
-
// Sync handle cache - MAJOR performance optimization
|
|
434
|
-
//
|
|
433
|
+
// Sync handle cache - MAJOR performance optimization (2-5x speedup)
|
|
434
|
+
// Uses readwrite-unsafe mode when available (no exclusive lock, allows external access)
|
|
435
|
+
// Falls back to readwrite with debounced release for older browsers
|
|
435
436
|
const syncHandleCache = new Map();
|
|
436
437
|
const syncHandleLastAccess = new Map();
|
|
438
|
+
const syncHandleActiveOps = new Map(); // Track active operations per handle
|
|
437
439
|
const MAX_HANDLES = 100;
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
440
|
+
|
|
441
|
+
// Track if readwrite-unsafe mode is supported (detected on first use)
|
|
442
|
+
let unsafeModeSupported = null;
|
|
443
|
+
|
|
444
|
+
// Handle release timing:
|
|
445
|
+
// - readwrite-unsafe: 30s idle timeout (no blocking, just memory management)
|
|
446
|
+
// - readwrite (fallback): 100ms debounce (need to release locks quickly)
|
|
447
|
+
let releaseTimer = null;
|
|
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
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function isHandleInUse(path) {
|
|
466
|
+
return (syncHandleActiveOps.get(path) || 0) > 0;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function scheduleHandleRelease() {
|
|
470
|
+
if (releaseTimer) {
|
|
471
|
+
clearTimeout(releaseTimer);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (unsafeModeSupported) {
|
|
475
|
+
// readwrite-unsafe: clean up handles idle for 30s
|
|
476
|
+
releaseTimer = setTimeout(() => {
|
|
477
|
+
releaseTimer = null;
|
|
478
|
+
const now = Date.now();
|
|
479
|
+
for (const [path, lastAccess] of syncHandleLastAccess) {
|
|
480
|
+
// Skip handles that are currently in use
|
|
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
|
+
}
|
|
451
488
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
+
}
|
|
455
509
|
}
|
|
456
510
|
|
|
457
511
|
async function getSyncHandle(filePath, create) {
|
|
458
512
|
const cached = syncHandleCache.get(filePath);
|
|
459
513
|
if (cached) {
|
|
514
|
+
// Renew last access time
|
|
460
515
|
syncHandleLastAccess.set(filePath, Date.now());
|
|
461
516
|
return cached;
|
|
462
517
|
}
|
|
@@ -471,10 +526,28 @@ async function getSyncHandle(filePath, create) {
|
|
|
471
526
|
}
|
|
472
527
|
|
|
473
528
|
const fh = await getFileHandle(filePath, create);
|
|
474
|
-
|
|
529
|
+
|
|
530
|
+
// Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
|
|
531
|
+
// Falls back to readwrite if not supported
|
|
532
|
+
let access;
|
|
533
|
+
if (unsafeModeSupported === null) {
|
|
534
|
+
// First time - detect support
|
|
535
|
+
try {
|
|
536
|
+
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
537
|
+
unsafeModeSupported = true;
|
|
538
|
+
} catch {
|
|
539
|
+
// Not supported, use default mode
|
|
540
|
+
access = await fh.createSyncAccessHandle();
|
|
541
|
+
unsafeModeSupported = false;
|
|
542
|
+
}
|
|
543
|
+
} else if (unsafeModeSupported) {
|
|
544
|
+
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
545
|
+
} else {
|
|
546
|
+
access = await fh.createSyncAccessHandle();
|
|
547
|
+
}
|
|
548
|
+
|
|
475
549
|
syncHandleCache.set(filePath, access);
|
|
476
550
|
syncHandleLastAccess.set(filePath, Date.now());
|
|
477
|
-
scheduleIdleCleanup();
|
|
478
551
|
return access;
|
|
479
552
|
}
|
|
480
553
|
|
|
@@ -563,12 +636,17 @@ async function getParentAndName(filePath) {
|
|
|
563
636
|
|
|
564
637
|
async function handleRead(filePath, payload) {
|
|
565
638
|
const access = await getSyncHandle(filePath, false);
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
639
|
+
beginHandleOp(filePath);
|
|
640
|
+
try {
|
|
641
|
+
const size = access.getSize();
|
|
642
|
+
const offset = payload?.offset || 0;
|
|
643
|
+
const len = payload?.len || (size - offset);
|
|
644
|
+
const buf = new Uint8Array(len);
|
|
645
|
+
const bytesRead = access.read(buf, { at: offset });
|
|
646
|
+
return { data: buf.slice(0, bytesRead) };
|
|
647
|
+
} finally {
|
|
648
|
+
endHandleOp(filePath);
|
|
649
|
+
}
|
|
572
650
|
}
|
|
573
651
|
|
|
574
652
|
// Non-blocking read using getFile() - does NOT lock the file
|
|
@@ -609,36 +687,52 @@ function handleReleaseAllHandles() {
|
|
|
609
687
|
}
|
|
610
688
|
syncHandleCache.clear();
|
|
611
689
|
syncHandleLastAccess.clear();
|
|
690
|
+
syncHandleActiveOps.clear();
|
|
612
691
|
return { success: true };
|
|
613
692
|
}
|
|
614
693
|
|
|
615
694
|
async function handleWrite(filePath, payload) {
|
|
616
695
|
const access = await getSyncHandle(filePath, true);
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
if (
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
696
|
+
beginHandleOp(filePath);
|
|
697
|
+
try {
|
|
698
|
+
if (payload?.data) {
|
|
699
|
+
const offset = payload.offset ?? 0;
|
|
700
|
+
if (offset === 0) access.truncate(0);
|
|
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);
|
|
623
708
|
}
|
|
624
|
-
return { success: true };
|
|
625
709
|
}
|
|
626
710
|
|
|
627
711
|
async function handleAppend(filePath, payload) {
|
|
628
712
|
const access = await getSyncHandle(filePath, true);
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
713
|
+
beginHandleOp(filePath);
|
|
714
|
+
try {
|
|
715
|
+
if (payload?.data) {
|
|
716
|
+
const size = access.getSize();
|
|
717
|
+
access.write(payload.data, { at: size });
|
|
718
|
+
if (payload?.flush !== false) access.flush();
|
|
719
|
+
}
|
|
720
|
+
return { success: true };
|
|
721
|
+
} finally {
|
|
722
|
+
endHandleOp(filePath);
|
|
633
723
|
}
|
|
634
|
-
return { success: true };
|
|
635
724
|
}
|
|
636
725
|
|
|
637
726
|
async function handleTruncate(filePath, payload) {
|
|
638
727
|
const access = await getSyncHandle(filePath, false);
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
728
|
+
beginHandleOp(filePath);
|
|
729
|
+
try {
|
|
730
|
+
access.truncate(payload?.len ?? 0);
|
|
731
|
+
access.flush();
|
|
732
|
+
return { success: true };
|
|
733
|
+
} finally {
|
|
734
|
+
endHandleOp(filePath);
|
|
735
|
+
}
|
|
642
736
|
}
|
|
643
737
|
|
|
644
738
|
async function handleStat(filePath) {
|
|
@@ -807,11 +901,18 @@ function handleFlush() {
|
|
|
807
901
|
}
|
|
808
902
|
|
|
809
903
|
function handlePurge() {
|
|
904
|
+
// Cancel any pending release timer
|
|
905
|
+
if (releaseTimer) {
|
|
906
|
+
clearTimeout(releaseTimer);
|
|
907
|
+
releaseTimer = null;
|
|
908
|
+
}
|
|
810
909
|
// Flush and close all cached sync handles
|
|
811
910
|
for (const [, handle] of syncHandleCache) {
|
|
812
911
|
try { handle.flush(); handle.close(); } catch {}
|
|
813
912
|
}
|
|
814
913
|
syncHandleCache.clear();
|
|
914
|
+
syncHandleLastAccess.clear();
|
|
915
|
+
syncHandleActiveOps.clear();
|
|
815
916
|
dirCache.clear();
|
|
816
917
|
cachedRoot = null;
|
|
817
918
|
return { success: true };
|
|
@@ -899,6 +1000,9 @@ async function handleMessage(msg) {
|
|
|
899
1000
|
} else {
|
|
900
1001
|
self.postMessage({ id, error: errorCode, code: errorCode });
|
|
901
1002
|
}
|
|
1003
|
+
} finally {
|
|
1004
|
+
// Schedule handle release (debounced - waits 100ms after last operation)
|
|
1005
|
+
scheduleHandleRelease();
|
|
902
1006
|
}
|
|
903
1007
|
}
|
|
904
1008
|
|