@componentor/fs 2.0.4 → 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
@@ -567,6 +567,47 @@ async function handleRead(filePath, payload) {
567
567
  return { data: buf.slice(0, bytesRead) };
568
568
  }
569
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
+
570
611
  async function handleWrite(filePath, payload) {
571
612
  const access = await getSyncHandle(filePath, true);
572
613
  if (payload?.data) {
@@ -776,6 +817,7 @@ async function processMessage(msg) {
776
817
  const { type, path, payload } = msg;
777
818
  switch (type) {
778
819
  case 'read': return handleRead(path, payload);
820
+ case 'readAsync': return handleReadAsync(path, payload);
779
821
  case 'write': return handleWrite(path, payload);
780
822
  case 'append': return handleAppend(path, payload);
781
823
  case 'truncate': return handleTruncate(path, payload);
@@ -789,6 +831,8 @@ async function processMessage(msg) {
789
831
  case 'copy': return handleCopy(path, payload);
790
832
  case 'flush': return handleFlush();
791
833
  case 'purge': return handlePurge();
834
+ case 'releaseHandle': return handleReleaseHandle(path);
835
+ case 'releaseAllHandles': return handleReleaseAllHandles();
792
836
  default: throw new Error('Unknown operation: ' + type);
793
837
  }
794
838
  }
@@ -2029,6 +2073,22 @@ var OPFSFileSystem = class _OPFSFileSystem {
2029
2073
  await this.fastCall("purge", "/");
2030
2074
  this.statCache.clear();
2031
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
+ }
2032
2092
  // Constants
2033
2093
  constants = constants;
2034
2094
  // --- FileHandle Implementation ---
@@ -2354,8 +2414,9 @@ var OPFSFileSystem = class _OPFSFileSystem {
2354
2414
  const self2 = this;
2355
2415
  let observer = null;
2356
2416
  let closed = false;
2357
- const callback = (records) => {
2417
+ const callback = async (records) => {
2358
2418
  if (closed) return;
2419
+ await self2.releaseAllHandles();
2359
2420
  for (const record of records) {
2360
2421
  if (record.type === "errored" || record.type === "unknown") continue;
2361
2422
  const filename = record.relativePathComponents.length > 0 ? record.relativePathComponents[record.relativePathComponents.length - 1] : basename(absPath);
@@ -2395,26 +2456,38 @@ var OPFSFileSystem = class _OPFSFileSystem {
2395
2456
  const entries = await this.promises.readdir(absPath);
2396
2457
  const currentEntries = new Set(entries);
2397
2458
  if (lastEntries !== null) {
2459
+ let hasChanges = false;
2460
+ const added = [];
2461
+ const removed = [];
2398
2462
  for (const entry of currentEntries) {
2399
2463
  if (!lastEntries.has(entry)) {
2400
- cb?.("rename", entry);
2464
+ added.push(entry);
2465
+ hasChanges = true;
2401
2466
  }
2402
2467
  }
2403
2468
  for (const entry of lastEntries) {
2404
2469
  if (!currentEntries.has(entry)) {
2405
- cb?.("rename", entry);
2470
+ removed.push(entry);
2471
+ hasChanges = true;
2406
2472
  }
2407
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
+ }
2408
2479
  }
2409
2480
  lastEntries = currentEntries;
2410
2481
  } else {
2411
2482
  if (lastMtimeMs !== null && stat.mtimeMs !== lastMtimeMs) {
2483
+ await this.releaseAllHandles();
2412
2484
  cb?.("change", basename(absPath));
2413
2485
  }
2414
2486
  lastMtimeMs = stat.mtimeMs;
2415
2487
  }
2416
2488
  } catch {
2417
2489
  if (lastMtimeMs !== null || lastEntries !== null) {
2490
+ await this.releaseAllHandles();
2418
2491
  cb?.("rename", basename(absPath));
2419
2492
  lastMtimeMs = null;
2420
2493
  lastEntries = null;
@@ -2448,6 +2521,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2448
2521
  const stat = await this.promises.stat(absPath);
2449
2522
  if (lastStat !== null) {
2450
2523
  if (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size) {
2524
+ await this.releaseAllHandles();
2451
2525
  cb?.(stat, lastStat);
2452
2526
  }
2453
2527
  }
@@ -2455,6 +2529,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2455
2529
  } catch {
2456
2530
  const emptyStat = createStats({ type: "file", size: 0, mtimeMs: 0, mode: 0 });
2457
2531
  if (lastStat !== null) {
2532
+ await this.releaseAllHandles();
2458
2533
  cb?.(emptyStat, lastStat);
2459
2534
  }
2460
2535
  lastStat = emptyStat;
@@ -2463,6 +2538,7 @@ var OPFSFileSystem = class _OPFSFileSystem {
2463
2538
  if (_OPFSFileSystem.hasNativeObserver && cb) {
2464
2539
  const self2 = this;
2465
2540
  const observerCallback = async () => {
2541
+ await self2.releaseAllHandles();
2466
2542
  try {
2467
2543
  const stat = await self2.promises.stat(absPath);
2468
2544
  if (lastStat !== null && (stat.mtimeMs !== lastStat.mtimeMs || stat.size !== lastStat.size)) {