@componentor/fs 2.0.6 → 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 +8 -14
- package/dist/index.cjs +89 -31
- 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 +89 -31
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +54 -39
- package/dist/kernel.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -446,20 +446,14 @@ 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
|
-
- Handle idle timeout now works for both Tier 1 and Tier 2
|
|
458
|
-
- Previously only Tier 1 kernel had idle release; Tier 2 kernel now also releases handles after 2s
|
|
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
|
+
**High-Performance Handle Caching with `readwrite-unsafe`:**
|
|
452
|
+
- Uses `readwrite-unsafe` mode (Chrome 121+) - no exclusive locks
|
|
453
|
+
- Zero per-operation overhead: cache lookup is a single Map.get()
|
|
454
|
+
- Browser extensions can access files while handles are cached
|
|
455
|
+
- LRU eviction when cache exceeds 100 handles
|
|
456
|
+
- Falls back to 100ms debounced release on older browsers (handles block)
|
|
463
457
|
|
|
464
458
|
### v2.0.2 (2025)
|
|
465
459
|
|
package/dist/index.cjs
CHANGED
|
@@ -430,57 +430,90 @@ 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
|
-
const syncHandleLastAccess = new Map();
|
|
437
437
|
const MAX_HANDLES = 100;
|
|
438
|
-
const HANDLE_IDLE_TIMEOUT = 2000;
|
|
439
|
-
let idleCleanupTimer = null;
|
|
440
438
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
439
|
+
// Track if readwrite-unsafe mode is supported (detected on first use)
|
|
440
|
+
let unsafeModeSupported = null;
|
|
441
|
+
|
|
442
|
+
// Debug tracing - set via 'setDebug' message
|
|
443
|
+
let debugTrace = false;
|
|
444
|
+
function trace(...args) {
|
|
445
|
+
if (debugTrace) console.log('[OPFS-T2]', ...args);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Minimal timer for legacy mode only
|
|
449
|
+
let releaseTimer = null;
|
|
450
|
+
const LEGACY_RELEASE_DELAY = 100;
|
|
451
|
+
|
|
452
|
+
function scheduleHandleRelease() {
|
|
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);
|
|
455
464
|
}
|
|
456
465
|
|
|
457
466
|
async function getSyncHandle(filePath, create) {
|
|
458
467
|
const cached = syncHandleCache.get(filePath);
|
|
459
468
|
if (cached) {
|
|
460
|
-
|
|
469
|
+
trace('Handle cache HIT: ' + filePath);
|
|
461
470
|
return cached;
|
|
462
471
|
}
|
|
463
472
|
|
|
464
473
|
// Evict oldest handles if cache is full
|
|
465
474
|
if (syncHandleCache.size >= MAX_HANDLES) {
|
|
466
475
|
const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
|
|
476
|
+
trace('LRU evicting ' + keys.length + ' handles');
|
|
467
477
|
for (const key of keys) {
|
|
468
478
|
const h = syncHandleCache.get(key);
|
|
469
|
-
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key);
|
|
479
|
+
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
|
|
470
480
|
}
|
|
471
481
|
}
|
|
472
482
|
|
|
473
483
|
const fh = await getFileHandle(filePath, create);
|
|
474
|
-
|
|
484
|
+
|
|
485
|
+
// Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
|
|
486
|
+
let access;
|
|
487
|
+
if (unsafeModeSupported === null) {
|
|
488
|
+
// First time - detect support
|
|
489
|
+
try {
|
|
490
|
+
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
491
|
+
unsafeModeSupported = true;
|
|
492
|
+
trace('readwrite-unsafe mode SUPPORTED - handles won\\'t block');
|
|
493
|
+
} catch {
|
|
494
|
+
// Not supported, use default mode
|
|
495
|
+
access = await fh.createSyncAccessHandle();
|
|
496
|
+
unsafeModeSupported = false;
|
|
497
|
+
trace('readwrite-unsafe mode NOT supported - using legacy mode');
|
|
498
|
+
}
|
|
499
|
+
} else if (unsafeModeSupported) {
|
|
500
|
+
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
501
|
+
} else {
|
|
502
|
+
access = await fh.createSyncAccessHandle();
|
|
503
|
+
}
|
|
504
|
+
|
|
475
505
|
syncHandleCache.set(filePath, access);
|
|
476
|
-
|
|
477
|
-
scheduleIdleCleanup();
|
|
506
|
+
trace('Handle ACQUIRED: ' + filePath + ' (cache size: ' + syncHandleCache.size + ')');
|
|
478
507
|
return access;
|
|
479
508
|
}
|
|
480
509
|
|
|
481
510
|
function closeSyncHandle(filePath) {
|
|
482
511
|
const h = syncHandleCache.get(filePath);
|
|
483
|
-
if (h) {
|
|
512
|
+
if (h) {
|
|
513
|
+
try { h.close(); } catch {}
|
|
514
|
+
syncHandleCache.delete(filePath);
|
|
515
|
+
trace('Handle RELEASED: ' + filePath);
|
|
516
|
+
}
|
|
484
517
|
}
|
|
485
518
|
|
|
486
519
|
function closeHandlesUnder(prefix) {
|
|
@@ -488,7 +521,6 @@ function closeHandlesUnder(prefix) {
|
|
|
488
521
|
if (p === prefix || p.startsWith(prefix + '/')) {
|
|
489
522
|
try { h.close(); } catch {}
|
|
490
523
|
syncHandleCache.delete(p);
|
|
491
|
-
syncHandleLastAccess.delete(p);
|
|
492
524
|
}
|
|
493
525
|
}
|
|
494
526
|
}
|
|
@@ -604,11 +636,10 @@ function handleReleaseHandle(filePath) {
|
|
|
604
636
|
|
|
605
637
|
// Force release ALL file handles - use before HMR notifications
|
|
606
638
|
function handleReleaseAllHandles() {
|
|
607
|
-
for (const
|
|
639
|
+
for (const h of syncHandleCache.values()) {
|
|
608
640
|
try { h.close(); } catch {}
|
|
609
641
|
}
|
|
610
642
|
syncHandleCache.clear();
|
|
611
|
-
syncHandleLastAccess.clear();
|
|
612
643
|
return { success: true };
|
|
613
644
|
}
|
|
614
645
|
|
|
@@ -618,7 +649,6 @@ async function handleWrite(filePath, payload) {
|
|
|
618
649
|
const offset = payload.offset ?? 0;
|
|
619
650
|
if (offset === 0) access.truncate(0);
|
|
620
651
|
access.write(payload.data, { at: offset });
|
|
621
|
-
// Only flush if explicitly requested (default: true for safety)
|
|
622
652
|
if (payload?.flush !== false) access.flush();
|
|
623
653
|
}
|
|
624
654
|
return { success: true };
|
|
@@ -807,8 +837,11 @@ function handleFlush() {
|
|
|
807
837
|
}
|
|
808
838
|
|
|
809
839
|
function handlePurge() {
|
|
810
|
-
|
|
811
|
-
|
|
840
|
+
if (releaseTimer) {
|
|
841
|
+
clearTimeout(releaseTimer);
|
|
842
|
+
releaseTimer = null;
|
|
843
|
+
}
|
|
844
|
+
for (const handle of syncHandleCache.values()) {
|
|
812
845
|
try { handle.flush(); handle.close(); } catch {}
|
|
813
846
|
}
|
|
814
847
|
syncHandleCache.clear();
|
|
@@ -837,6 +870,10 @@ async function processMessage(msg) {
|
|
|
837
870
|
case 'purge': return handlePurge();
|
|
838
871
|
case 'releaseHandle': return handleReleaseHandle(path);
|
|
839
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 };
|
|
840
877
|
default: throw new Error('Unknown operation: ' + type);
|
|
841
878
|
}
|
|
842
879
|
}
|
|
@@ -899,6 +936,9 @@ async function handleMessage(msg) {
|
|
|
899
936
|
} else {
|
|
900
937
|
self.postMessage({ id, error: errorCode, code: errorCode });
|
|
901
938
|
}
|
|
939
|
+
} finally {
|
|
940
|
+
// Schedule handle release (debounced - waits 100ms after last operation)
|
|
941
|
+
scheduleHandleRelease();
|
|
902
942
|
}
|
|
903
943
|
}
|
|
904
944
|
|
|
@@ -1690,6 +1730,24 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
1690
1730
|
this.syncCall("purge", "/");
|
|
1691
1731
|
this.statCache.clear();
|
|
1692
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
|
+
}
|
|
1693
1751
|
accessSync(filePath, _mode) {
|
|
1694
1752
|
const exists = this.existsSync(filePath);
|
|
1695
1753
|
if (!exists) {
|