@componentor/fs 2.0.2 → 2.0.4

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
@@ -446,10 +446,18 @@ Another tab or operation has the file open. The library uses `navigator.locks` t
446
446
 
447
447
  ## Changelog
448
448
 
449
+ ### v2.0.3 (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
+ - Reduced handle idle timeout from 5s to 2s for faster external tool access
455
+ - Added tests verifying handles are properly released after idle timeout
456
+
449
457
  ### v2.0.2 (2025)
450
458
 
451
459
  **Improvements:**
452
- - Sync access handles now auto-release after 5 seconds of inactivity
460
+ - Sync access handles now auto-release after idle timeout
453
461
  - Allows external tools (like OPFS Chrome extension) to access files when idle
454
462
  - Maintains full performance during active operations
455
463
 
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) return 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
  }