@componentor/fs 2.0.7 → 2.0.9

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.d.cts CHANGED
@@ -315,6 +315,28 @@ declare class OPFSFileSystem {
315
315
  * Use between major operations to ensure clean state.
316
316
  */
317
317
  purgeSync(): void;
318
+ /**
319
+ * Enable or disable debug tracing for handle operations.
320
+ * When enabled, logs handle cache hits, acquisitions, releases, and mode information.
321
+ * @param enabled - Whether to enable debug tracing
322
+ * @returns Debug state information including unsafeModeSupported and cache size
323
+ */
324
+ setDebugSync(enabled: boolean): {
325
+ debugTrace: boolean;
326
+ unsafeModeSupported: boolean;
327
+ cacheSize: number;
328
+ };
329
+ /**
330
+ * Enable or disable debug tracing for handle operations (async version).
331
+ * When enabled, logs handle cache hits, acquisitions, releases, and mode information.
332
+ * @param enabled - Whether to enable debug tracing
333
+ * @returns Debug state information including unsafeModeSupported and cache size
334
+ */
335
+ setDebug(enabled: boolean): Promise<{
336
+ debugTrace: boolean;
337
+ unsafeModeSupported: boolean;
338
+ cacheSize: number;
339
+ }>;
318
340
  accessSync(filePath: string, _mode?: number): void;
319
341
  openSync(filePath: string, flags?: string | number): number;
320
342
  closeSync(fd: number): void;
package/dist/index.d.ts CHANGED
@@ -315,6 +315,28 @@ declare class OPFSFileSystem {
315
315
  * Use between major operations to ensure clean state.
316
316
  */
317
317
  purgeSync(): void;
318
+ /**
319
+ * Enable or disable debug tracing for handle operations.
320
+ * When enabled, logs handle cache hits, acquisitions, releases, and mode information.
321
+ * @param enabled - Whether to enable debug tracing
322
+ * @returns Debug state information including unsafeModeSupported and cache size
323
+ */
324
+ setDebugSync(enabled: boolean): {
325
+ debugTrace: boolean;
326
+ unsafeModeSupported: boolean;
327
+ cacheSize: number;
328
+ };
329
+ /**
330
+ * Enable or disable debug tracing for handle operations (async version).
331
+ * When enabled, logs handle cache hits, acquisitions, releases, and mode information.
332
+ * @param enabled - Whether to enable debug tracing
333
+ * @returns Debug state information including unsafeModeSupported and cache size
334
+ */
335
+ setDebug(enabled: boolean): Promise<{
336
+ debugTrace: boolean;
337
+ unsafeModeSupported: boolean;
338
+ cacheSize: number;
339
+ }>;
318
340
  accessSync(filePath: string, _mode?: number): void;
319
341
  openSync(filePath: string, flags?: string | number): number;
320
342
  closeSync(fd: number): void;
package/dist/index.js CHANGED
@@ -430,111 +430,75 @@ const dirCache = new Map();
430
430
  // Uses readwrite-unsafe mode when available (no exclusive lock, allows external access)
431
431
  // Falls back to readwrite with debounced release for older browsers
432
432
  const syncHandleCache = new Map();
433
- const syncHandleLastAccess = new Map();
434
- const syncHandleActiveOps = new Map(); // Track active operations per handle
435
433
  const MAX_HANDLES = 100;
436
434
 
437
435
  // Track if readwrite-unsafe mode is supported (detected on first use)
438
436
  let unsafeModeSupported = null;
439
437
 
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)
438
+ // Debug tracing - set via 'setDebug' message
439
+ let debugTrace = false;
440
+ function trace(...args) {
441
+ if (debugTrace) console.log('[OPFS-T2]', ...args);
442
+ }
443
+
444
+ // Handle release timing
445
+ // - Legacy mode (readwrite): 100ms delay (handles block, release ASAP)
446
+ // - Unsafe mode (readwrite-unsafe): 500ms delay (handles don't block each other,
447
+ // but DO block external tools using default mode like OPFS Explorer)
443
448
  let releaseTimer = null;
444
- const UNSAFE_IDLE_TIMEOUT = 30000;
445
449
  const LEGACY_RELEASE_DELAY = 100;
450
+ const UNSAFE_RELEASE_DELAY = 500;
446
451
 
447
- // Track active operations to prevent releasing handles in use
448
- function beginHandleOp(path) {
449
- syncHandleActiveOps.set(path, (syncHandleActiveOps.get(path) || 0) + 1);
450
- }
452
+ function scheduleHandleRelease() {
453
+ if (releaseTimer) return; // Already scheduled
451
454
 
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
- }
455
+ const delay = unsafeModeSupported ? UNSAFE_RELEASE_DELAY : LEGACY_RELEASE_DELAY;
460
456
 
461
- function isHandleInUse(path) {
462
- return (syncHandleActiveOps.get(path) || 0) > 0;
463
- }
464
-
465
- function scheduleHandleRelease() {
466
- if (releaseTimer) {
467
- clearTimeout(releaseTimer);
468
- }
457
+ releaseTimer = setTimeout(() => {
458
+ releaseTimer = null;
459
+ const count = syncHandleCache.size;
460
+ if (count === 0) return;
469
461
 
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
- }
484
- }
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
- }
462
+ for (const h of syncHandleCache.values()) {
463
+ try { h.flush(); h.close(); } catch {}
464
+ }
465
+ syncHandleCache.clear();
466
+ trace('Released ' + count + ' handles (' + (unsafeModeSupported ? 'unsafe' : 'legacy') + ' mode, ' + delay + 'ms delay)');
467
+ }, delay);
505
468
  }
506
469
 
507
470
  async function getSyncHandle(filePath, create) {
508
471
  const cached = syncHandleCache.get(filePath);
509
472
  if (cached) {
510
- // Renew last access time
511
- syncHandleLastAccess.set(filePath, Date.now());
473
+ trace('Handle cache HIT: ' + filePath);
512
474
  return cached;
513
475
  }
514
476
 
515
477
  // Evict oldest handles if cache is full
516
478
  if (syncHandleCache.size >= MAX_HANDLES) {
517
479
  const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
480
+ trace('LRU evicting ' + keys.length + ' handles');
518
481
  for (const key of keys) {
519
482
  const h = syncHandleCache.get(key);
520
- if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); syncHandleLastAccess.delete(key); }
483
+ if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
521
484
  }
522
485
  }
523
486
 
524
487
  const fh = await getFileHandle(filePath, create);
525
488
 
526
489
  // Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
527
- // Falls back to readwrite if not supported
528
490
  let access;
529
491
  if (unsafeModeSupported === null) {
530
492
  // First time - detect support
531
493
  try {
532
494
  access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
533
495
  unsafeModeSupported = true;
496
+ trace('readwrite-unsafe mode SUPPORTED - handles won\\'t block');
534
497
  } catch {
535
498
  // Not supported, use default mode
536
499
  access = await fh.createSyncAccessHandle();
537
500
  unsafeModeSupported = false;
501
+ trace('readwrite-unsafe mode NOT supported - using legacy mode');
538
502
  }
539
503
  } else if (unsafeModeSupported) {
540
504
  access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
@@ -543,13 +507,17 @@ async function getSyncHandle(filePath, create) {
543
507
  }
544
508
 
545
509
  syncHandleCache.set(filePath, access);
546
- syncHandleLastAccess.set(filePath, Date.now());
510
+ trace('Handle ACQUIRED: ' + filePath + ' (cache size: ' + syncHandleCache.size + ')');
547
511
  return access;
548
512
  }
549
513
 
550
514
  function closeSyncHandle(filePath) {
551
515
  const h = syncHandleCache.get(filePath);
552
- if (h) { try { h.close(); } catch {} syncHandleCache.delete(filePath); syncHandleLastAccess.delete(filePath); }
516
+ if (h) {
517
+ try { h.close(); } catch {}
518
+ syncHandleCache.delete(filePath);
519
+ trace('Handle RELEASED: ' + filePath);
520
+ }
553
521
  }
554
522
 
555
523
  function closeHandlesUnder(prefix) {
@@ -557,7 +525,6 @@ function closeHandlesUnder(prefix) {
557
525
  if (p === prefix || p.startsWith(prefix + '/')) {
558
526
  try { h.close(); } catch {}
559
527
  syncHandleCache.delete(p);
560
- syncHandleLastAccess.delete(p);
561
528
  }
562
529
  }
563
530
  }
@@ -632,17 +599,12 @@ async function getParentAndName(filePath) {
632
599
 
633
600
  async function handleRead(filePath, payload) {
634
601
  const access = await getSyncHandle(filePath, false);
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
- }
602
+ const size = access.getSize();
603
+ const offset = payload?.offset || 0;
604
+ const len = payload?.len || (size - offset);
605
+ const buf = new Uint8Array(len);
606
+ const bytesRead = access.read(buf, { at: offset });
607
+ return { data: buf.slice(0, bytesRead) };
646
608
  }
647
609
 
648
610
  // Non-blocking read using getFile() - does NOT lock the file
@@ -678,57 +640,39 @@ function handleReleaseHandle(filePath) {
678
640
 
679
641
  // Force release ALL file handles - use before HMR notifications
680
642
  function handleReleaseAllHandles() {
681
- for (const [p, h] of syncHandleCache) {
643
+ for (const h of syncHandleCache.values()) {
682
644
  try { h.close(); } catch {}
683
645
  }
684
646
  syncHandleCache.clear();
685
- syncHandleLastAccess.clear();
686
- syncHandleActiveOps.clear();
687
647
  return { success: true };
688
648
  }
689
649
 
690
650
  async function handleWrite(filePath, payload) {
691
651
  const access = await getSyncHandle(filePath, true);
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);
652
+ if (payload?.data) {
653
+ const offset = payload.offset ?? 0;
654
+ if (offset === 0) access.truncate(0);
655
+ access.write(payload.data, { at: offset });
656
+ if (payload?.flush !== false) access.flush();
704
657
  }
658
+ return { success: true };
705
659
  }
706
660
 
707
661
  async function handleAppend(filePath, payload) {
708
662
  const access = await getSyncHandle(filePath, true);
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);
663
+ if (payload?.data) {
664
+ const size = access.getSize();
665
+ access.write(payload.data, { at: size });
666
+ if (payload?.flush !== false) access.flush();
719
667
  }
668
+ return { success: true };
720
669
  }
721
670
 
722
671
  async function handleTruncate(filePath, payload) {
723
672
  const access = await getSyncHandle(filePath, false);
724
- beginHandleOp(filePath);
725
- try {
726
- access.truncate(payload?.len ?? 0);
727
- access.flush();
728
- return { success: true };
729
- } finally {
730
- endHandleOp(filePath);
731
- }
673
+ access.truncate(payload?.len ?? 0);
674
+ access.flush();
675
+ return { success: true };
732
676
  }
733
677
 
734
678
  async function handleStat(filePath) {
@@ -897,18 +841,14 @@ function handleFlush() {
897
841
  }
898
842
 
899
843
  function handlePurge() {
900
- // Cancel any pending release timer
901
844
  if (releaseTimer) {
902
845
  clearTimeout(releaseTimer);
903
846
  releaseTimer = null;
904
847
  }
905
- // Flush and close all cached sync handles
906
- for (const [, handle] of syncHandleCache) {
848
+ for (const handle of syncHandleCache.values()) {
907
849
  try { handle.flush(); handle.close(); } catch {}
908
850
  }
909
851
  syncHandleCache.clear();
910
- syncHandleLastAccess.clear();
911
- syncHandleActiveOps.clear();
912
852
  dirCache.clear();
913
853
  cachedRoot = null;
914
854
  return { success: true };
@@ -934,6 +874,10 @@ async function processMessage(msg) {
934
874
  case 'purge': return handlePurge();
935
875
  case 'releaseHandle': return handleReleaseHandle(path);
936
876
  case 'releaseAllHandles': return handleReleaseAllHandles();
877
+ case 'setDebug':
878
+ debugTrace = !!payload?.enabled;
879
+ trace('Debug tracing ' + (debugTrace ? 'ENABLED' : 'DISABLED') + ', unsafeMode: ' + unsafeModeSupported);
880
+ return { success: true, debugTrace, unsafeModeSupported, cacheSize: syncHandleCache.size };
937
881
  default: throw new Error('Unknown operation: ' + type);
938
882
  }
939
883
  }
@@ -1002,22 +946,27 @@ async function handleMessage(msg) {
1002
946
  }
1003
947
  }
1004
948
 
1005
- // Process queued messages after ready
1006
- function processQueue() {
1007
- while (messageQueue.length > 0) {
949
+ // Process queued messages with concurrency limit
950
+ // Allows multiple operations to run in parallel but prevents overwhelming the worker
951
+ const MAX_CONCURRENT = 8;
952
+ let activeOperations = 0;
953
+
954
+ async function processQueue() {
955
+ while (messageQueue.length > 0 && activeOperations < MAX_CONCURRENT) {
1008
956
  const msg = messageQueue.shift();
1009
- handleMessage(msg);
957
+ activeOperations++;
958
+ handleMessage(msg).finally(() => {
959
+ activeOperations--;
960
+ processQueue(); // Process next queued message
961
+ });
1010
962
  }
1011
963
  }
1012
964
 
1013
- // Handle messages directly - no serialization needed because:
1014
- // - Tier 2: Client awaits response before sending next message
1015
- // - Each OPFSFileSystem instance has its own worker
965
+ // Queue messages and process with controlled concurrency
1016
966
  self.onmessage = (event) => {
967
+ messageQueue.push(event.data);
1017
968
  if (isReady) {
1018
- handleMessage(event.data);
1019
- } else {
1020
- messageQueue.push(event.data);
969
+ processQueue();
1021
970
  }
1022
971
  };
1023
972
 
@@ -1790,6 +1739,24 @@ var OPFSFileSystem = class _OPFSFileSystem {
1790
1739
  this.syncCall("purge", "/");
1791
1740
  this.statCache.clear();
1792
1741
  }
1742
+ /**
1743
+ * Enable or disable debug tracing for handle operations.
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
+ setDebugSync(enabled) {
1749
+ return this.syncCall("setDebug", "/", { enabled });
1750
+ }
1751
+ /**
1752
+ * Enable or disable debug tracing for handle operations (async version).
1753
+ * When enabled, logs handle cache hits, acquisitions, releases, and mode information.
1754
+ * @param enabled - Whether to enable debug tracing
1755
+ * @returns Debug state information including unsafeModeSupported and cache size
1756
+ */
1757
+ async setDebug(enabled) {
1758
+ return this.asyncCall("setDebug", "/", { enabled });
1759
+ }
1793
1760
  accessSync(filePath, _mode) {
1794
1761
  const exists = this.existsSync(filePath);
1795
1762
  if (!exists) {