@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 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
- **Smart Handle Caching with `readwrite-unsafe`:**
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
- // 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
- }
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
- function isHandleInUse(path) {
466
- return (syncHandleActiveOps.get(path) || 0) > 0;
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 (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
- }
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
- // Renew last access time
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); syncHandleLastAccess.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
- syncHandleLastAccess.set(filePath, Date.now());
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) { try { h.close(); } catch {} syncHandleCache.delete(filePath); syncHandleLastAccess.delete(filePath); }
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
- 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
- }
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 [p, h] of syncHandleCache) {
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
- 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);
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
- 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);
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
- beginHandleOp(filePath);
729
- try {
730
- access.truncate(payload?.len ?? 0);
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
- // Flush and close all cached sync handles
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) {