@componentor/fs 2.0.4 → 2.0.6

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/README.md CHANGED
@@ -446,11 +446,18 @@ Another tab or operation has the file open. The library uses `navigator.locks` t
446
446
 
447
447
  ## Changelog
448
448
 
449
- ### v2.0.3 (2025)
449
+ ### v2.0.5 (2025)
450
+
451
+ **Bug Fixes:**
452
+ - Fixed idle timeout condition (`>` to `>=`) - handles now release at exactly 2s instead of ~4s
453
+
454
+ ### v2.0.4 (2025)
450
455
 
451
456
  **Bug Fixes:**
452
457
  - Handle idle timeout now works for both Tier 1 and Tier 2
453
458
  - Previously only Tier 1 kernel had idle release; Tier 2 kernel now also releases handles after 2s
459
+
460
+ ### v2.0.3 (2025)
454
461
  - Reduced handle idle timeout from 5s to 2s for faster external tool access
455
462
  - Added tests verifying handles are properly released after idle timeout
456
463
 
package/dist/index.cjs CHANGED
@@ -444,7 +444,7 @@ function scheduleIdleCleanup() {
444
444
  idleCleanupTimer = null;
445
445
  const now = Date.now();
446
446
  for (const [p, lastAccess] of syncHandleLastAccess) {
447
- if (now - lastAccess > HANDLE_IDLE_TIMEOUT) {
447
+ if (now - lastAccess >= HANDLE_IDLE_TIMEOUT) {
448
448
  const h = syncHandleCache.get(p);
449
449
  if (h) { try { h.flush(); h.close(); } catch {} syncHandleCache.delete(p); }
450
450
  syncHandleLastAccess.delete(p);
@@ -571,6 +571,47 @@ async function handleRead(filePath, payload) {
571
571
  return { data: buf.slice(0, bytesRead) };
572
572
  }
573
573
 
574
+ // Non-blocking read using getFile() - does NOT lock the file
575
+ // Use this for HMR scenarios where external tools need to modify files
576
+ async function handleReadAsync(filePath, payload) {
577
+ const parts = parsePath(filePath);
578
+ const fileName = parts.pop();
579
+ if (!fileName) throw new Error('Invalid file path');
580
+ const dir = parts.length > 0 ? await getDirectoryHandle(parts, false) : await getRoot();
581
+ const fh = await dir.getFileHandle(fileName);
582
+ const file = await fh.getFile();
583
+
584
+ const offset = payload?.offset || 0;
585
+ const len = payload?.len || (file.size - offset);
586
+
587
+ if (offset === 0 && len === file.size) {
588
+ // Fast path: read entire file
589
+ const buf = new Uint8Array(await file.arrayBuffer());
590
+ return { data: buf };
591
+ }
592
+
593
+ // Partial read using slice
594
+ const slice = file.slice(offset, offset + len);
595
+ const buf = new Uint8Array(await slice.arrayBuffer());
596
+ return { data: buf };
597
+ }
598
+
599
+ // Force release a file handle - allows external tools to modify the file
600
+ function handleReleaseHandle(filePath) {
601
+ closeSyncHandle(filePath);
602
+ return { success: true };
603
+ }
604
+
605
+ // Force release ALL file handles - use before HMR notifications
606
+ function handleReleaseAllHandles() {
607
+ for (const [p, h] of syncHandleCache) {
608
+ try { h.close(); } catch {}
609
+ }
610
+ syncHandleCache.clear();
611
+ syncHandleLastAccess.clear();
612
+ return { success: true };
613
+ }
614
+
574
615
  async function handleWrite(filePath, payload) {
575
616
  const access = await getSyncHandle(filePath, true);
576
617
  if (payload?.data) {
@@ -780,6 +821,7 @@ async function processMessage(msg) {
780
821
  const { type, path, payload } = msg;
781
822
  switch (type) {
782
823
  case 'read': return handleRead(path, payload);
824
+ case 'readAsync': return handleReadAsync(path, payload);
783
825
  case 'write': return handleWrite(path, payload);
784
826
  case 'append': return handleAppend(path, payload);
785
827
  case 'truncate': return handleTruncate(path, payload);
@@ -793,6 +835,8 @@ async function processMessage(msg) {
793
835
  case 'copy': return handleCopy(path, payload);
794
836
  case 'flush': return handleFlush();
795
837
  case 'purge': return handlePurge();
838
+ case 'releaseHandle': return handleReleaseHandle(path);
839
+ case 'releaseAllHandles': return handleReleaseAllHandles();
796
840
  default: throw new Error('Unknown operation: ' + type);
797
841
  }
798
842
  }
@@ -2033,6 +2077,22 @@ var OPFSFileSystem = class _OPFSFileSystem {
2033
2077
  await this.fastCall("purge", "/");
2034
2078
  this.statCache.clear();
2035
2079
  }
2080
+ /**
2081
+ * Release all cached file handles.
2082
+ * Call this before expecting external tools (OPFS Explorer, browser console, etc.)
2083
+ * to modify files. This allows external access without waiting for the idle timeout.
2084
+ * Unlike purge(), this only releases file handles without clearing directory caches.
2085
+ */
2086
+ async releaseAllHandles() {
2087
+ await this.fastCall("releaseAllHandles", "/");
2088
+ }
2089
+ /**
2090
+ * Release a specific file's handle.
2091
+ * Use this when you know a specific file needs to be externally modified.
2092
+ */
2093
+ async releaseHandle(filePath) {
2094
+ await this.fastCall("releaseHandle", filePath);
2095
+ }
2036
2096
  // Constants
2037
2097
  constants = constants;
2038
2098
  // --- FileHandle Implementation ---
@@ -2358,8 +2418,9 @@ var OPFSFileSystem = class _OPFSFileSystem {
2358
2418
  const self2 = this;
2359
2419
  let observer = null;
2360
2420
  let closed = false;
2361
- const callback = (records) => {
2421
+ const callback = async (records) => {
2362
2422
  if (closed) return;
2423
+ await self2.releaseAllHandles();
2363
2424
  for (const record of records) {
2364
2425
  if (record.type === "errored" || record.type === "unknown") continue;
2365
2426
  const filename = record.relativePathComponents.length > 0 ? record.relativePathComponents[record.relativePathComponents.length - 1] : basename(absPath);
@@ -2399,26 +2460,38 @@ var OPFSFileSystem = class _OPFSFileSystem {
2399
2460
  const entries = await this.promises.readdir(absPath);
2400
2461
  const currentEntries = new Set(entries);
2401
2462
  if (lastEntries !== null) {
2463
+ let hasChanges = false;
2464
+ const added = [];
2465
+ const removed = [];
2402
2466
  for (const entry of currentEntries) {
2403
2467
  if (!lastEntries.has(entry)) {
2404
- cb?.("rename", entry);
2468
+ added.push(entry);
2469
+ hasChanges = true;
2405
2470
  }
2406
2471
  }
2407
2472
  for (const entry of lastEntries) {
2408
2473
  if (!currentEntries.has(entry)) {
2409
- cb?.("rename", entry);
2474
+ removed.push(entry);
2475
+ hasChanges = true;
2410
2476
  }
2411
2477
  }
2478
+ if (hasChanges) {
2479
+ await this.releaseAllHandles();
2480
+ for (const entry of added) cb?.("rename", entry);
2481
+ for (const entry of removed) cb?.("rename", entry);
2482
+ }
2412
2483
  }
2413
2484
  lastEntries = currentEntries;
2414
2485
  } else {
2415
2486
  if (lastMtimeMs !== null && stat.mtimeMs !== lastMtimeMs) {
2487
+ await this.releaseAllHandles();
2416
2488
  cb?.("change", basename(absPath));
2417
2489
  }
2418
2490
  lastMtimeMs = stat.mtimeMs;
2419
2491
  }
2420
2492
  } catch {
2421
2493
  if (lastMtimeMs !== null || lastEntries !== null) {
2494
+ await this.releaseAllHandles();
2422
2495
  cb?.("rename", basename(absPath));
2423
2496
  lastMtimeMs = null;
2424
2497
  lastEntries = null;
@@ -2452,6 +2525,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2452
2525
  const stat = await this.promises.stat(absPath);
2453
2526
  if (lastStat !== null) {
2454
2527
  if (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size) {
2528
+ await this.releaseAllHandles();
2455
2529
  cb?.(stat, lastStat);
2456
2530
  }
2457
2531
  }
@@ -2459,6 +2533,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2459
2533
  } catch {
2460
2534
  const emptyStat = createStats({ type: "file", size: 0, mtimeMs: 0, mode: 0 });
2461
2535
  if (lastStat !== null) {
2536
+ await this.releaseAllHandles();
2462
2537
  cb?.(emptyStat, lastStat);
2463
2538
  }
2464
2539
  lastStat = emptyStat;
@@ -2467,6 +2542,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2467
2542
  if (_OPFSFileSystem.hasNativeObserver && cb) {
2468
2543
  const self2 = this;
2469
2544
  const observerCallback = async () => {
2545
+ await self2.releaseAllHandles();
2470
2546
  try {
2471
2547
  const stat = await self2.promises.stat(absPath);
2472
2548
  if (lastStat !== null && (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size)) {