@componentor/fs 2.0.2 → 2.0.5

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
@@ -373,6 +373,18 @@ declare class OPFSFileSystem {
373
373
  * Async purge - clears all kernel caches
374
374
  */
375
375
  purge(): Promise<void>;
376
+ /**
377
+ * Release all cached file handles.
378
+ * Call this before expecting external tools (OPFS Explorer, browser console, etc.)
379
+ * to modify files. This allows external access without waiting for the idle timeout.
380
+ * Unlike purge(), this only releases file handles without clearing directory caches.
381
+ */
382
+ releaseAllHandles(): Promise<void>;
383
+ /**
384
+ * Release a specific file's handle.
385
+ * Use this when you know a specific file needs to be externally modified.
386
+ */
387
+ releaseHandle(filePath: string): Promise<void>;
376
388
  constants: {
377
389
  readonly F_OK: 0;
378
390
  readonly R_OK: 4;
package/dist/index.d.ts CHANGED
@@ -373,6 +373,18 @@ declare class OPFSFileSystem {
373
373
  * Async purge - clears all kernel caches
374
374
  */
375
375
  purge(): Promise<void>;
376
+ /**
377
+ * Release all cached file handles.
378
+ * Call this before expecting external tools (OPFS Explorer, browser console, etc.)
379
+ * to modify files. This allows external access without waiting for the idle timeout.
380
+ * Unlike purge(), this only releases file handles without clearing directory caches.
381
+ */
382
+ releaseAllHandles(): Promise<void>;
383
+ /**
384
+ * Release a specific file's handle.
385
+ * Use this when you know a specific file needs to be externally modified.
386
+ */
387
+ releaseHandle(filePath: string): Promise<void>;
376
388
  constants: {
377
389
  readonly F_OK: 0;
378
390
  readonly R_OK: 4;
package/dist/index.js CHANGED
@@ -427,31 +427,56 @@ let cachedRoot = null;
427
427
  const dirCache = new Map();
428
428
 
429
429
  // Sync handle cache - MAJOR performance optimization
430
+ // Handles auto-release after idle timeout to allow external tools to access files
430
431
  const syncHandleCache = new Map();
432
+ const syncHandleLastAccess = new Map();
431
433
  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);
447
+ }
448
+ }
449
+ if (syncHandleCache.size > 0) scheduleIdleCleanup();
450
+ }, HANDLE_IDLE_TIMEOUT);
451
+ }
432
452
 
433
453
  async function getSyncHandle(filePath, create) {
434
454
  const cached = syncHandleCache.get(filePath);
435
- if (cached) return cached;
455
+ if (cached) {
456
+ syncHandleLastAccess.set(filePath, Date.now());
457
+ return cached;
458
+ }
436
459
 
437
460
  // Evict oldest handles if cache is full
438
461
  if (syncHandleCache.size >= MAX_HANDLES) {
439
462
  const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
440
463
  for (const key of keys) {
441
464
  const h = syncHandleCache.get(key);
442
- if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
465
+ if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); syncHandleLastAccess.delete(key); }
443
466
  }
444
467
  }
445
468
 
446
469
  const fh = await getFileHandle(filePath, create);
447
470
  const access = await fh.createSyncAccessHandle();
448
471
  syncHandleCache.set(filePath, access);
472
+ syncHandleLastAccess.set(filePath, Date.now());
473
+ scheduleIdleCleanup();
449
474
  return access;
450
475
  }
451
476
 
452
477
  function closeSyncHandle(filePath) {
453
478
  const h = syncHandleCache.get(filePath);
454
- if (h) { try { h.close(); } catch {} syncHandleCache.delete(filePath); }
479
+ if (h) { try { h.close(); } catch {} syncHandleCache.delete(filePath); syncHandleLastAccess.delete(filePath); }
455
480
  }
456
481
 
457
482
  function closeHandlesUnder(prefix) {
@@ -459,6 +484,7 @@ function closeHandlesUnder(prefix) {
459
484
  if (p === prefix || p.startsWith(prefix + '/')) {
460
485
  try { h.close(); } catch {}
461
486
  syncHandleCache.delete(p);
487
+ syncHandleLastAccess.delete(p);
462
488
  }
463
489
  }
464
490
  }
@@ -541,6 +567,47 @@ async function handleRead(filePath, payload) {
541
567
  return { data: buf.slice(0, bytesRead) };
542
568
  }
543
569
 
570
+ // Non-blocking read using getFile() - does NOT lock the file
571
+ // Use this for HMR scenarios where external tools need to modify files
572
+ async function handleReadAsync(filePath, payload) {
573
+ const parts = parsePath(filePath);
574
+ const fileName = parts.pop();
575
+ if (!fileName) throw new Error('Invalid file path');
576
+ const dir = parts.length > 0 ? await getDirectoryHandle(parts, false) : await getRoot();
577
+ const fh = await dir.getFileHandle(fileName);
578
+ const file = await fh.getFile();
579
+
580
+ const offset = payload?.offset || 0;
581
+ const len = payload?.len || (file.size - offset);
582
+
583
+ if (offset === 0 && len === file.size) {
584
+ // Fast path: read entire file
585
+ const buf = new Uint8Array(await file.arrayBuffer());
586
+ return { data: buf };
587
+ }
588
+
589
+ // Partial read using slice
590
+ const slice = file.slice(offset, offset + len);
591
+ const buf = new Uint8Array(await slice.arrayBuffer());
592
+ return { data: buf };
593
+ }
594
+
595
+ // Force release a file handle - allows external tools to modify the file
596
+ function handleReleaseHandle(filePath) {
597
+ closeSyncHandle(filePath);
598
+ return { success: true };
599
+ }
600
+
601
+ // Force release ALL file handles - use before HMR notifications
602
+ function handleReleaseAllHandles() {
603
+ for (const [p, h] of syncHandleCache) {
604
+ try { h.close(); } catch {}
605
+ }
606
+ syncHandleCache.clear();
607
+ syncHandleLastAccess.clear();
608
+ return { success: true };
609
+ }
610
+
544
611
  async function handleWrite(filePath, payload) {
545
612
  const access = await getSyncHandle(filePath, true);
546
613
  if (payload?.data) {
@@ -750,6 +817,7 @@ async function processMessage(msg) {
750
817
  const { type, path, payload } = msg;
751
818
  switch (type) {
752
819
  case 'read': return handleRead(path, payload);
820
+ case 'readAsync': return handleReadAsync(path, payload);
753
821
  case 'write': return handleWrite(path, payload);
754
822
  case 'append': return handleAppend(path, payload);
755
823
  case 'truncate': return handleTruncate(path, payload);
@@ -763,6 +831,8 @@ async function processMessage(msg) {
763
831
  case 'copy': return handleCopy(path, payload);
764
832
  case 'flush': return handleFlush();
765
833
  case 'purge': return handlePurge();
834
+ case 'releaseHandle': return handleReleaseHandle(path);
835
+ case 'releaseAllHandles': return handleReleaseAllHandles();
766
836
  default: throw new Error('Unknown operation: ' + type);
767
837
  }
768
838
  }
@@ -2003,6 +2073,22 @@ var OPFSFileSystem = class _OPFSFileSystem {
2003
2073
  await this.fastCall("purge", "/");
2004
2074
  this.statCache.clear();
2005
2075
  }
2076
+ /**
2077
+ * Release all cached file handles.
2078
+ * Call this before expecting external tools (OPFS Explorer, browser console, etc.)
2079
+ * to modify files. This allows external access without waiting for the idle timeout.
2080
+ * Unlike purge(), this only releases file handles without clearing directory caches.
2081
+ */
2082
+ async releaseAllHandles() {
2083
+ await this.fastCall("releaseAllHandles", "/");
2084
+ }
2085
+ /**
2086
+ * Release a specific file's handle.
2087
+ * Use this when you know a specific file needs to be externally modified.
2088
+ */
2089
+ async releaseHandle(filePath) {
2090
+ await this.fastCall("releaseHandle", filePath);
2091
+ }
2006
2092
  // Constants
2007
2093
  constants = constants;
2008
2094
  // --- FileHandle Implementation ---
@@ -2328,8 +2414,9 @@ var OPFSFileSystem = class _OPFSFileSystem {
2328
2414
  const self2 = this;
2329
2415
  let observer = null;
2330
2416
  let closed = false;
2331
- const callback = (records) => {
2417
+ const callback = async (records) => {
2332
2418
  if (closed) return;
2419
+ await self2.releaseAllHandles();
2333
2420
  for (const record of records) {
2334
2421
  if (record.type === "errored" || record.type === "unknown") continue;
2335
2422
  const filename = record.relativePathComponents.length > 0 ? record.relativePathComponents[record.relativePathComponents.length - 1] : basename(absPath);
@@ -2369,26 +2456,38 @@ var OPFSFileSystem = class _OPFSFileSystem {
2369
2456
  const entries = await this.promises.readdir(absPath);
2370
2457
  const currentEntries = new Set(entries);
2371
2458
  if (lastEntries !== null) {
2459
+ let hasChanges = false;
2460
+ const added = [];
2461
+ const removed = [];
2372
2462
  for (const entry of currentEntries) {
2373
2463
  if (!lastEntries.has(entry)) {
2374
- cb?.("rename", entry);
2464
+ added.push(entry);
2465
+ hasChanges = true;
2375
2466
  }
2376
2467
  }
2377
2468
  for (const entry of lastEntries) {
2378
2469
  if (!currentEntries.has(entry)) {
2379
- cb?.("rename", entry);
2470
+ removed.push(entry);
2471
+ hasChanges = true;
2380
2472
  }
2381
2473
  }
2474
+ if (hasChanges) {
2475
+ await this.releaseAllHandles();
2476
+ for (const entry of added) cb?.("rename", entry);
2477
+ for (const entry of removed) cb?.("rename", entry);
2478
+ }
2382
2479
  }
2383
2480
  lastEntries = currentEntries;
2384
2481
  } else {
2385
2482
  if (lastMtimeMs !== null && stat.mtimeMs !== lastMtimeMs) {
2483
+ await this.releaseAllHandles();
2386
2484
  cb?.("change", basename(absPath));
2387
2485
  }
2388
2486
  lastMtimeMs = stat.mtimeMs;
2389
2487
  }
2390
2488
  } catch {
2391
2489
  if (lastMtimeMs !== null || lastEntries !== null) {
2490
+ await this.releaseAllHandles();
2392
2491
  cb?.("rename", basename(absPath));
2393
2492
  lastMtimeMs = null;
2394
2493
  lastEntries = null;
@@ -2422,6 +2521,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2422
2521
  const stat = await this.promises.stat(absPath);
2423
2522
  if (lastStat !== null) {
2424
2523
  if (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size) {
2524
+ await this.releaseAllHandles();
2425
2525
  cb?.(stat, lastStat);
2426
2526
  }
2427
2527
  }
@@ -2429,6 +2529,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2429
2529
  } catch {
2430
2530
  const emptyStat = createStats({ type: "file", size: 0, mtimeMs: 0, mode: 0 });
2431
2531
  if (lastStat !== null) {
2532
+ await this.releaseAllHandles();
2432
2533
  cb?.(emptyStat, lastStat);
2433
2534
  }
2434
2535
  lastStat = emptyStat;
@@ -2437,6 +2538,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2437
2538
  if (_OPFSFileSystem.hasNativeObserver && cb) {
2438
2539
  const self2 = this;
2439
2540
  const observerCallback = async () => {
2541
+ await self2.releaseAllHandles();
2440
2542
  try {
2441
2543
  const stat = await self2.promises.stat(absPath);
2442
2544
  if (lastStat !== null && (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size)) {