@componentor/fs 2.0.6 → 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
@@ -426,57 +426,90 @@ 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
- const syncHandleLastAccess = new Map();
433
433
  const MAX_HANDLES = 100;
434
- const HANDLE_IDLE_TIMEOUT = 2000;
435
- let idleCleanupTimer = null;
436
434
 
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);
447
- }
448
- }
449
- if (syncHandleCache.size > 0) scheduleIdleCleanup();
450
- }, HANDLE_IDLE_TIMEOUT);
435
+ // Track if readwrite-unsafe mode is supported (detected on first use)
436
+ let unsafeModeSupported = null;
437
+
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
+ // Minimal timer for legacy mode only
445
+ let releaseTimer = null;
446
+ const LEGACY_RELEASE_DELAY = 100;
447
+
448
+ function scheduleHandleRelease() {
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);
451
460
  }
452
461
 
453
462
  async function getSyncHandle(filePath, create) {
454
463
  const cached = syncHandleCache.get(filePath);
455
464
  if (cached) {
456
- syncHandleLastAccess.set(filePath, Date.now());
465
+ trace('Handle cache HIT: ' + filePath);
457
466
  return cached;
458
467
  }
459
468
 
460
469
  // Evict oldest handles if cache is full
461
470
  if (syncHandleCache.size >= MAX_HANDLES) {
462
471
  const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
472
+ trace('LRU evicting ' + keys.length + ' handles');
463
473
  for (const key of keys) {
464
474
  const h = syncHandleCache.get(key);
465
- if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); syncHandleLastAccess.delete(key); }
475
+ if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
466
476
  }
467
477
  }
468
478
 
469
479
  const fh = await getFileHandle(filePath, create);
470
- const access = await fh.createSyncAccessHandle();
480
+
481
+ // Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
482
+ let access;
483
+ if (unsafeModeSupported === null) {
484
+ // First time - detect support
485
+ try {
486
+ access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
487
+ unsafeModeSupported = true;
488
+ trace('readwrite-unsafe mode SUPPORTED - handles won\\'t block');
489
+ } catch {
490
+ // Not supported, use default mode
491
+ access = await fh.createSyncAccessHandle();
492
+ unsafeModeSupported = false;
493
+ trace('readwrite-unsafe mode NOT supported - using legacy mode');
494
+ }
495
+ } else if (unsafeModeSupported) {
496
+ access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
497
+ } else {
498
+ access = await fh.createSyncAccessHandle();
499
+ }
500
+
471
501
  syncHandleCache.set(filePath, access);
472
- syncHandleLastAccess.set(filePath, Date.now());
473
- scheduleIdleCleanup();
502
+ trace('Handle ACQUIRED: ' + filePath + ' (cache size: ' + syncHandleCache.size + ')');
474
503
  return access;
475
504
  }
476
505
 
477
506
  function closeSyncHandle(filePath) {
478
507
  const h = syncHandleCache.get(filePath);
479
- 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
+ }
480
513
  }
481
514
 
482
515
  function closeHandlesUnder(prefix) {
@@ -484,7 +517,6 @@ function closeHandlesUnder(prefix) {
484
517
  if (p === prefix || p.startsWith(prefix + '/')) {
485
518
  try { h.close(); } catch {}
486
519
  syncHandleCache.delete(p);
487
- syncHandleLastAccess.delete(p);
488
520
  }
489
521
  }
490
522
  }
@@ -600,11 +632,10 @@ function handleReleaseHandle(filePath) {
600
632
 
601
633
  // Force release ALL file handles - use before HMR notifications
602
634
  function handleReleaseAllHandles() {
603
- for (const [p, h] of syncHandleCache) {
635
+ for (const h of syncHandleCache.values()) {
604
636
  try { h.close(); } catch {}
605
637
  }
606
638
  syncHandleCache.clear();
607
- syncHandleLastAccess.clear();
608
639
  return { success: true };
609
640
  }
610
641
 
@@ -614,7 +645,6 @@ async function handleWrite(filePath, payload) {
614
645
  const offset = payload.offset ?? 0;
615
646
  if (offset === 0) access.truncate(0);
616
647
  access.write(payload.data, { at: offset });
617
- // Only flush if explicitly requested (default: true for safety)
618
648
  if (payload?.flush !== false) access.flush();
619
649
  }
620
650
  return { success: true };
@@ -803,8 +833,11 @@ function handleFlush() {
803
833
  }
804
834
 
805
835
  function handlePurge() {
806
- // Flush and close all cached sync handles
807
- for (const [, handle] of syncHandleCache) {
836
+ if (releaseTimer) {
837
+ clearTimeout(releaseTimer);
838
+ releaseTimer = null;
839
+ }
840
+ for (const handle of syncHandleCache.values()) {
808
841
  try { handle.flush(); handle.close(); } catch {}
809
842
  }
810
843
  syncHandleCache.clear();
@@ -833,6 +866,10 @@ async function processMessage(msg) {
833
866
  case 'purge': return handlePurge();
834
867
  case 'releaseHandle': return handleReleaseHandle(path);
835
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 };
836
873
  default: throw new Error('Unknown operation: ' + type);
837
874
  }
838
875
  }
@@ -895,6 +932,9 @@ async function handleMessage(msg) {
895
932
  } else {
896
933
  self.postMessage({ id, error: errorCode, code: errorCode });
897
934
  }
935
+ } finally {
936
+ // Schedule handle release (debounced - waits 100ms after last operation)
937
+ scheduleHandleRelease();
898
938
  }
899
939
  }
900
940
 
@@ -1686,6 +1726,24 @@ var OPFSFileSystem = class _OPFSFileSystem {
1686
1726
  this.syncCall("purge", "/");
1687
1727
  this.statCache.clear();
1688
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
+ }
1689
1747
  accessSync(filePath, _mode) {
1690
1748
  const exists = this.existsSync(filePath);
1691
1749
  if (!exists) {