@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/README.md +3 -1
- package/dist/index.cjs +79 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +79 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -446,11 +446,13 @@ Another tab or operation has the file open. The library uses `navigator.locks` t
|
|
|
446
446
|
|
|
447
447
|
## Changelog
|
|
448
448
|
|
|
449
|
-
### v2.0.
|
|
449
|
+
### v2.0.4 (2025)
|
|
450
450
|
|
|
451
451
|
**Bug Fixes:**
|
|
452
452
|
- Handle idle timeout now works for both Tier 1 and Tier 2
|
|
453
453
|
- Previously only Tier 1 kernel had idle release; Tier 2 kernel now also releases handles after 2s
|
|
454
|
+
|
|
455
|
+
### v2.0.3 (2025)
|
|
454
456
|
- Reduced handle idle timeout from 5s to 2s for faster external tool access
|
|
455
457
|
- Added tests verifying handles are properly released after idle timeout
|
|
456
458
|
|
package/dist/index.cjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)) {
|