@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/dist/index.js CHANGED
@@ -426,33 +426,88 @@ let isReady = false;
426
426
  let cachedRoot = null;
427
427
  const dirCache = new Map();
428
428
 
429
- // Sync handle cache - MAJOR performance optimization
430
- // Handles auto-release after idle timeout to allow external tools to access files
429
+ // Sync handle cache - MAJOR performance optimization (2-5x speedup)
430
+ // Uses readwrite-unsafe mode when available (no exclusive lock, allows external access)
431
+ // Falls back to readwrite with debounced release for older browsers
431
432
  const syncHandleCache = new Map();
432
433
  const syncHandleLastAccess = new Map();
434
+ const syncHandleActiveOps = new Map(); // Track active operations per handle
433
435
  const MAX_HANDLES = 100;
434
- const HANDLE_IDLE_TIMEOUT = 2000;
435
- let idleCleanupTimer = null;
436
-
437
- function scheduleIdleCleanup() {
438
- if (idleCleanupTimer) return;
439
- idleCleanupTimer = setTimeout(() => {
440
- idleCleanupTimer = null;
441
- const now = Date.now();
442
- for (const [p, lastAccess] of syncHandleLastAccess) {
443
- if (now - lastAccess >= HANDLE_IDLE_TIMEOUT) {
444
- const h = syncHandleCache.get(p);
445
- if (h) { try { h.flush(); h.close(); } catch {} syncHandleCache.delete(p); }
446
- syncHandleLastAccess.delete(p);
436
+
437
+ // Track if readwrite-unsafe mode is supported (detected on first use)
438
+ let unsafeModeSupported = null;
439
+
440
+ // Handle release timing:
441
+ // - readwrite-unsafe: 30s idle timeout (no blocking, just memory management)
442
+ // - readwrite (fallback): 100ms debounce (need to release locks quickly)
443
+ let releaseTimer = null;
444
+ const UNSAFE_IDLE_TIMEOUT = 30000;
445
+ const LEGACY_RELEASE_DELAY = 100;
446
+
447
+ // Track active operations to prevent releasing handles in use
448
+ function beginHandleOp(path) {
449
+ syncHandleActiveOps.set(path, (syncHandleActiveOps.get(path) || 0) + 1);
450
+ }
451
+
452
+ function endHandleOp(path) {
453
+ const count = syncHandleActiveOps.get(path) || 0;
454
+ if (count <= 1) {
455
+ syncHandleActiveOps.delete(path);
456
+ } else {
457
+ syncHandleActiveOps.set(path, count - 1);
458
+ }
459
+ }
460
+
461
+ function isHandleInUse(path) {
462
+ return (syncHandleActiveOps.get(path) || 0) > 0;
463
+ }
464
+
465
+ function scheduleHandleRelease() {
466
+ if (releaseTimer) {
467
+ clearTimeout(releaseTimer);
468
+ }
469
+
470
+ if (unsafeModeSupported) {
471
+ // readwrite-unsafe: clean up handles idle for 30s
472
+ releaseTimer = setTimeout(() => {
473
+ releaseTimer = null;
474
+ const now = Date.now();
475
+ for (const [path, lastAccess] of syncHandleLastAccess) {
476
+ // Skip handles that are currently in use
477
+ if (isHandleInUse(path)) continue;
478
+ if (now - lastAccess >= UNSAFE_IDLE_TIMEOUT) {
479
+ const h = syncHandleCache.get(path);
480
+ if (h) { try { h.flush(); h.close(); } catch {} }
481
+ syncHandleCache.delete(path);
482
+ syncHandleLastAccess.delete(path);
483
+ }
447
484
  }
448
- }
449
- if (syncHandleCache.size > 0) scheduleIdleCleanup();
450
- }, HANDLE_IDLE_TIMEOUT);
485
+ // Reschedule if there are still handles
486
+ if (syncHandleCache.size > 0) {
487
+ scheduleHandleRelease();
488
+ }
489
+ }, UNSAFE_IDLE_TIMEOUT);
490
+ } else {
491
+ // Legacy readwrite: release all handles after 100ms idle
492
+ releaseTimer = setTimeout(() => {
493
+ releaseTimer = null;
494
+ if (syncHandleCache.size > 0) {
495
+ for (const [path, h] of syncHandleCache) {
496
+ // Skip handles that are currently in use
497
+ if (isHandleInUse(path)) continue;
498
+ try { h.flush(); h.close(); } catch {}
499
+ syncHandleCache.delete(path);
500
+ syncHandleLastAccess.delete(path);
501
+ }
502
+ }
503
+ }, LEGACY_RELEASE_DELAY);
504
+ }
451
505
  }
452
506
 
453
507
  async function getSyncHandle(filePath, create) {
454
508
  const cached = syncHandleCache.get(filePath);
455
509
  if (cached) {
510
+ // Renew last access time
456
511
  syncHandleLastAccess.set(filePath, Date.now());
457
512
  return cached;
458
513
  }
@@ -467,10 +522,28 @@ async function getSyncHandle(filePath, create) {
467
522
  }
468
523
 
469
524
  const fh = await getFileHandle(filePath, create);
470
- const access = await fh.createSyncAccessHandle();
525
+
526
+ // Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
527
+ // Falls back to readwrite if not supported
528
+ let access;
529
+ if (unsafeModeSupported === null) {
530
+ // First time - detect support
531
+ try {
532
+ access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
533
+ unsafeModeSupported = true;
534
+ } catch {
535
+ // Not supported, use default mode
536
+ access = await fh.createSyncAccessHandle();
537
+ unsafeModeSupported = false;
538
+ }
539
+ } else if (unsafeModeSupported) {
540
+ access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
541
+ } else {
542
+ access = await fh.createSyncAccessHandle();
543
+ }
544
+
471
545
  syncHandleCache.set(filePath, access);
472
546
  syncHandleLastAccess.set(filePath, Date.now());
473
- scheduleIdleCleanup();
474
547
  return access;
475
548
  }
476
549
 
@@ -559,12 +632,17 @@ async function getParentAndName(filePath) {
559
632
 
560
633
  async function handleRead(filePath, payload) {
561
634
  const access = await getSyncHandle(filePath, false);
562
- const size = access.getSize();
563
- const offset = payload?.offset || 0;
564
- const len = payload?.len || (size - offset);
565
- const buf = new Uint8Array(len);
566
- const bytesRead = access.read(buf, { at: offset });
567
- return { data: buf.slice(0, bytesRead) };
635
+ beginHandleOp(filePath);
636
+ try {
637
+ const size = access.getSize();
638
+ const offset = payload?.offset || 0;
639
+ const len = payload?.len || (size - offset);
640
+ const buf = new Uint8Array(len);
641
+ const bytesRead = access.read(buf, { at: offset });
642
+ return { data: buf.slice(0, bytesRead) };
643
+ } finally {
644
+ endHandleOp(filePath);
645
+ }
568
646
  }
569
647
 
570
648
  // Non-blocking read using getFile() - does NOT lock the file
@@ -605,36 +683,52 @@ function handleReleaseAllHandles() {
605
683
  }
606
684
  syncHandleCache.clear();
607
685
  syncHandleLastAccess.clear();
686
+ syncHandleActiveOps.clear();
608
687
  return { success: true };
609
688
  }
610
689
 
611
690
  async function handleWrite(filePath, payload) {
612
691
  const access = await getSyncHandle(filePath, true);
613
- if (payload?.data) {
614
- const offset = payload.offset ?? 0;
615
- if (offset === 0) access.truncate(0);
616
- access.write(payload.data, { at: offset });
617
- // Only flush if explicitly requested (default: true for safety)
618
- if (payload?.flush !== false) access.flush();
692
+ beginHandleOp(filePath);
693
+ try {
694
+ if (payload?.data) {
695
+ const offset = payload.offset ?? 0;
696
+ if (offset === 0) access.truncate(0);
697
+ access.write(payload.data, { at: offset });
698
+ // Only flush if explicitly requested (default: true for safety)
699
+ if (payload?.flush !== false) access.flush();
700
+ }
701
+ return { success: true };
702
+ } finally {
703
+ endHandleOp(filePath);
619
704
  }
620
- return { success: true };
621
705
  }
622
706
 
623
707
  async function handleAppend(filePath, payload) {
624
708
  const access = await getSyncHandle(filePath, true);
625
- if (payload?.data) {
626
- const size = access.getSize();
627
- access.write(payload.data, { at: size });
628
- if (payload?.flush !== false) access.flush();
709
+ beginHandleOp(filePath);
710
+ try {
711
+ if (payload?.data) {
712
+ const size = access.getSize();
713
+ access.write(payload.data, { at: size });
714
+ if (payload?.flush !== false) access.flush();
715
+ }
716
+ return { success: true };
717
+ } finally {
718
+ endHandleOp(filePath);
629
719
  }
630
- return { success: true };
631
720
  }
632
721
 
633
722
  async function handleTruncate(filePath, payload) {
634
723
  const access = await getSyncHandle(filePath, false);
635
- access.truncate(payload?.len ?? 0);
636
- access.flush();
637
- return { success: true };
724
+ beginHandleOp(filePath);
725
+ try {
726
+ access.truncate(payload?.len ?? 0);
727
+ access.flush();
728
+ return { success: true };
729
+ } finally {
730
+ endHandleOp(filePath);
731
+ }
638
732
  }
639
733
 
640
734
  async function handleStat(filePath) {
@@ -803,11 +897,18 @@ function handleFlush() {
803
897
  }
804
898
 
805
899
  function handlePurge() {
900
+ // Cancel any pending release timer
901
+ if (releaseTimer) {
902
+ clearTimeout(releaseTimer);
903
+ releaseTimer = null;
904
+ }
806
905
  // Flush and close all cached sync handles
807
906
  for (const [, handle] of syncHandleCache) {
808
907
  try { handle.flush(); handle.close(); } catch {}
809
908
  }
810
909
  syncHandleCache.clear();
910
+ syncHandleLastAccess.clear();
911
+ syncHandleActiveOps.clear();
811
912
  dirCache.clear();
812
913
  cachedRoot = null;
813
914
  return { success: true };
@@ -895,6 +996,9 @@ async function handleMessage(msg) {
895
996
  } else {
896
997
  self.postMessage({ id, error: errorCode, code: errorCode });
897
998
  }
999
+ } finally {
1000
+ // Schedule handle release (debounced - waits 100ms after last operation)
1001
+ scheduleHandleRelease();
898
1002
  }
899
1003
  }
900
1004