@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/README.md +11 -1
- package/dist/index.cjs +108 -6
- 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 +108 -6
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +1 -1
- package/dist/kernel.js.map +1 -1
- package/package.json +1 -1
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)
|
|
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
|
-
|
|
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
|
-
|
|
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)) {
|