@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/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,67 @@ 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)
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
- }
438
+ // Debug tracing - set via 'setDebug' message
439
+ let debugTrace = false;
440
+ function trace(...args) {
441
+ if (debugTrace) console.log('[OPFS-T2]', ...args);
459
442
  }
460
443
 
461
- function isHandleInUse(path) {
462
- return (syncHandleActiveOps.get(path) || 0) > 0;
463
- }
444
+ // Minimal timer for legacy mode only
445
+ let releaseTimer = null;
446
+ const LEGACY_RELEASE_DELAY = 100;
464
447
 
465
448
  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
- }
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
- }
449
+ if (unsafeModeSupported) return; // No release needed for readwrite-unsafe
450
+ if (releaseTimer) return; // Already scheduled
451
+ releaseTimer = setTimeout(() => {
452
+ releaseTimer = null;
453
+ const count = syncHandleCache.size;
454
+ for (const h of syncHandleCache.values()) {
455
+ try { h.flush(); h.close(); } catch {}
456
+ }
457
+ syncHandleCache.clear();
458
+ trace('Released ' + count + ' handles (legacy mode debounce)');
459
+ }, LEGACY_RELEASE_DELAY);
505
460
  }
506
461
 
507
462
  async function getSyncHandle(filePath, create) {
508
463
  const cached = syncHandleCache.get(filePath);
509
464
  if (cached) {
510
- // Renew last access time
511
- syncHandleLastAccess.set(filePath, Date.now());
465
+ trace('Handle cache HIT: ' + filePath);
512
466
  return cached;
513
467
  }
514
468
 
515
469
  // Evict oldest handles if cache is full
516
470
  if (syncHandleCache.size >= MAX_HANDLES) {
517
471
  const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
472
+ trace('LRU evicting ' + keys.length + ' handles');
518
473
  for (const key of keys) {
519
474
  const h = syncHandleCache.get(key);
520
- if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); syncHandleLastAccess.delete(key); }
475
+ if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
521
476
  }
522
477
  }
523
478
 
524
479
  const fh = await getFileHandle(filePath, create);
525
480
 
526
481
  // Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
527
- // Falls back to readwrite if not supported
528
482
  let access;
529
483
  if (unsafeModeSupported === null) {
530
484
  // First time - detect support
531
485
  try {
532
486
  access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
533
487
  unsafeModeSupported = true;
488
+ trace('readwrite-unsafe mode SUPPORTED - handles won\\'t block');
534
489
  } catch {
535
490
  // Not supported, use default mode
536
491
  access = await fh.createSyncAccessHandle();
537
492
  unsafeModeSupported = false;
493
+ trace('readwrite-unsafe mode NOT supported - using legacy mode');
538
494
  }
539
495
  } else if (unsafeModeSupported) {
540
496
  access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
@@ -543,13 +499,17 @@ async function getSyncHandle(filePath, create) {
543
499
  }
544
500
 
545
501
  syncHandleCache.set(filePath, access);
546
- syncHandleLastAccess.set(filePath, Date.now());
502
+ trace('Handle ACQUIRED: ' + filePath + ' (cache size: ' + syncHandleCache.size + ')');
547
503
  return access;
548
504
  }
549
505
 
550
506
  function closeSyncHandle(filePath) {
551
507
  const h = syncHandleCache.get(filePath);
552
- if (h) { try { h.close(); } catch {} syncHandleCache.delete(filePath); syncHandleLastAccess.delete(filePath); }
508
+ if (h) {
509
+ try { h.close(); } catch {}
510
+ syncHandleCache.delete(filePath);
511
+ trace('Handle RELEASED: ' + filePath);
512
+ }
553
513
  }
554
514
 
555
515
  function closeHandlesUnder(prefix) {
@@ -557,7 +517,6 @@ function closeHandlesUnder(prefix) {
557
517
  if (p === prefix || p.startsWith(prefix + '/')) {
558
518
  try { h.close(); } catch {}
559
519
  syncHandleCache.delete(p);
560
- syncHandleLastAccess.delete(p);
561
520
  }
562
521
  }
563
522
  }
@@ -632,17 +591,12 @@ async function getParentAndName(filePath) {
632
591
 
633
592
  async function handleRead(filePath, payload) {
634
593
  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
- }
594
+ const size = access.getSize();
595
+ const offset = payload?.offset || 0;
596
+ const len = payload?.len || (size - offset);
597
+ const buf = new Uint8Array(len);
598
+ const bytesRead = access.read(buf, { at: offset });
599
+ return { data: buf.slice(0, bytesRead) };
646
600
  }
647
601
 
648
602
  // Non-blocking read using getFile() - does NOT lock the file
@@ -678,57 +632,39 @@ function handleReleaseHandle(filePath) {
678
632
 
679
633
  // Force release ALL file handles - use before HMR notifications
680
634
  function handleReleaseAllHandles() {
681
- for (const [p, h] of syncHandleCache) {
635
+ for (const h of syncHandleCache.values()) {
682
636
  try { h.close(); } catch {}
683
637
  }
684
638
  syncHandleCache.clear();
685
- syncHandleLastAccess.clear();
686
- syncHandleActiveOps.clear();
687
639
  return { success: true };
688
640
  }
689
641
 
690
642
  async function handleWrite(filePath, payload) {
691
643
  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);
644
+ if (payload?.data) {
645
+ const offset = payload.offset ?? 0;
646
+ if (offset === 0) access.truncate(0);
647
+ access.write(payload.data, { at: offset });
648
+ if (payload?.flush !== false) access.flush();
704
649
  }
650
+ return { success: true };
705
651
  }
706
652
 
707
653
  async function handleAppend(filePath, payload) {
708
654
  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);
655
+ if (payload?.data) {
656
+ const size = access.getSize();
657
+ access.write(payload.data, { at: size });
658
+ if (payload?.flush !== false) access.flush();
719
659
  }
660
+ return { success: true };
720
661
  }
721
662
 
722
663
  async function handleTruncate(filePath, payload) {
723
664
  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
- }
665
+ access.truncate(payload?.len ?? 0);
666
+ access.flush();
667
+ return { success: true };
732
668
  }
733
669
 
734
670
  async function handleStat(filePath) {
@@ -897,18 +833,14 @@ function handleFlush() {
897
833
  }
898
834
 
899
835
  function handlePurge() {
900
- // Cancel any pending release timer
901
836
  if (releaseTimer) {
902
837
  clearTimeout(releaseTimer);
903
838
  releaseTimer = null;
904
839
  }
905
- // Flush and close all cached sync handles
906
- for (const [, handle] of syncHandleCache) {
840
+ for (const handle of syncHandleCache.values()) {
907
841
  try { handle.flush(); handle.close(); } catch {}
908
842
  }
909
843
  syncHandleCache.clear();
910
- syncHandleLastAccess.clear();
911
- syncHandleActiveOps.clear();
912
844
  dirCache.clear();
913
845
  cachedRoot = null;
914
846
  return { success: true };
@@ -934,6 +866,10 @@ async function processMessage(msg) {
934
866
  case 'purge': return handlePurge();
935
867
  case 'releaseHandle': return handleReleaseHandle(path);
936
868
  case 'releaseAllHandles': return handleReleaseAllHandles();
869
+ case 'setDebug':
870
+ debugTrace = !!payload?.enabled;
871
+ trace('Debug tracing ' + (debugTrace ? 'ENABLED' : 'DISABLED') + ', unsafeMode: ' + unsafeModeSupported);
872
+ return { success: true, debugTrace, unsafeModeSupported, cacheSize: syncHandleCache.size };
937
873
  default: throw new Error('Unknown operation: ' + type);
938
874
  }
939
875
  }
@@ -1790,6 +1726,24 @@ var OPFSFileSystem = class _OPFSFileSystem {
1790
1726
  this.syncCall("purge", "/");
1791
1727
  this.statCache.clear();
1792
1728
  }
1729
+ /**
1730
+ * Enable or disable debug tracing for handle operations.
1731
+ * When enabled, logs handle cache hits, acquisitions, releases, and mode information.
1732
+ * @param enabled - Whether to enable debug tracing
1733
+ * @returns Debug state information including unsafeModeSupported and cache size
1734
+ */
1735
+ setDebugSync(enabled) {
1736
+ return this.syncCall("setDebug", "/", { enabled });
1737
+ }
1738
+ /**
1739
+ * Enable or disable debug tracing for handle operations (async version).
1740
+ * When enabled, logs handle cache hits, acquisitions, releases, and mode information.
1741
+ * @param enabled - Whether to enable debug tracing
1742
+ * @returns Debug state information including unsafeModeSupported and cache size
1743
+ */
1744
+ async setDebug(enabled) {
1745
+ return this.asyncCall("setDebug", "/", { enabled });
1746
+ }
1793
1747
  accessSync(filePath, _mode) {
1794
1748
  const exists = this.existsSync(filePath);
1795
1749
  if (!exists) {