@componentor/fs 2.0.7 → 2.0.8
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 -5
- package/dist/index.cjs +73 -119
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +73 -119
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +52 -110
- package/dist/kernel.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -315,6 +315,28 @@ declare class OPFSFileSystem {
|
|
|
315
315
|
* Use between major operations to ensure clean state.
|
|
316
316
|
*/
|
|
317
317
|
purgeSync(): void;
|
|
318
|
+
/**
|
|
319
|
+
* Enable or disable debug tracing for handle operations.
|
|
320
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
321
|
+
* @param enabled - Whether to enable debug tracing
|
|
322
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
323
|
+
*/
|
|
324
|
+
setDebugSync(enabled: boolean): {
|
|
325
|
+
debugTrace: boolean;
|
|
326
|
+
unsafeModeSupported: boolean;
|
|
327
|
+
cacheSize: number;
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Enable or disable debug tracing for handle operations (async version).
|
|
331
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
332
|
+
* @param enabled - Whether to enable debug tracing
|
|
333
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
334
|
+
*/
|
|
335
|
+
setDebug(enabled: boolean): Promise<{
|
|
336
|
+
debugTrace: boolean;
|
|
337
|
+
unsafeModeSupported: boolean;
|
|
338
|
+
cacheSize: number;
|
|
339
|
+
}>;
|
|
318
340
|
accessSync(filePath: string, _mode?: number): void;
|
|
319
341
|
openSync(filePath: string, flags?: string | number): number;
|
|
320
342
|
closeSync(fd: number): void;
|
package/dist/index.d.ts
CHANGED
|
@@ -315,6 +315,28 @@ declare class OPFSFileSystem {
|
|
|
315
315
|
* Use between major operations to ensure clean state.
|
|
316
316
|
*/
|
|
317
317
|
purgeSync(): void;
|
|
318
|
+
/**
|
|
319
|
+
* Enable or disable debug tracing for handle operations.
|
|
320
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
321
|
+
* @param enabled - Whether to enable debug tracing
|
|
322
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
323
|
+
*/
|
|
324
|
+
setDebugSync(enabled: boolean): {
|
|
325
|
+
debugTrace: boolean;
|
|
326
|
+
unsafeModeSupported: boolean;
|
|
327
|
+
cacheSize: number;
|
|
328
|
+
};
|
|
329
|
+
/**
|
|
330
|
+
* Enable or disable debug tracing for handle operations (async version).
|
|
331
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
332
|
+
* @param enabled - Whether to enable debug tracing
|
|
333
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
334
|
+
*/
|
|
335
|
+
setDebug(enabled: boolean): Promise<{
|
|
336
|
+
debugTrace: boolean;
|
|
337
|
+
unsafeModeSupported: boolean;
|
|
338
|
+
cacheSize: number;
|
|
339
|
+
}>;
|
|
318
340
|
accessSync(filePath: string, _mode?: number): void;
|
|
319
341
|
openSync(filePath: string, flags?: string | number): number;
|
|
320
342
|
closeSync(fd: number): void;
|
package/dist/index.js
CHANGED
|
@@ -430,111 +430,67 @@ const dirCache = new Map();
|
|
|
430
430
|
// Uses readwrite-unsafe mode when available (no exclusive lock, allows external access)
|
|
431
431
|
// Falls back to readwrite with debounced release for older browsers
|
|
432
432
|
const syncHandleCache = new Map();
|
|
433
|
-
const syncHandleLastAccess = new Map();
|
|
434
|
-
const syncHandleActiveOps = new Map(); // Track active operations per handle
|
|
435
433
|
const MAX_HANDLES = 100;
|
|
436
434
|
|
|
437
435
|
// Track if readwrite-unsafe mode is supported (detected on first use)
|
|
438
436
|
let unsafeModeSupported = null;
|
|
439
437
|
|
|
440
|
-
//
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const UNSAFE_IDLE_TIMEOUT = 30000;
|
|
445
|
-
const LEGACY_RELEASE_DELAY = 100;
|
|
446
|
-
|
|
447
|
-
// Track active operations to prevent releasing handles in use
|
|
448
|
-
function beginHandleOp(path) {
|
|
449
|
-
syncHandleActiveOps.set(path, (syncHandleActiveOps.get(path) || 0) + 1);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
function endHandleOp(path) {
|
|
453
|
-
const count = syncHandleActiveOps.get(path) || 0;
|
|
454
|
-
if (count <= 1) {
|
|
455
|
-
syncHandleActiveOps.delete(path);
|
|
456
|
-
} else {
|
|
457
|
-
syncHandleActiveOps.set(path, count - 1);
|
|
458
|
-
}
|
|
438
|
+
// Debug tracing - set via 'setDebug' message
|
|
439
|
+
let debugTrace = false;
|
|
440
|
+
function trace(...args) {
|
|
441
|
+
if (debugTrace) console.log('[OPFS-T2]', ...args);
|
|
459
442
|
}
|
|
460
443
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
444
|
+
// Minimal timer for legacy mode only
|
|
445
|
+
let releaseTimer = null;
|
|
446
|
+
const LEGACY_RELEASE_DELAY = 100;
|
|
464
447
|
|
|
465
448
|
function scheduleHandleRelease() {
|
|
466
|
-
if (
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
if (isHandleInUse(path)) continue;
|
|
478
|
-
if (now - lastAccess >= UNSAFE_IDLE_TIMEOUT) {
|
|
479
|
-
const h = syncHandleCache.get(path);
|
|
480
|
-
if (h) { try { h.flush(); h.close(); } catch {} }
|
|
481
|
-
syncHandleCache.delete(path);
|
|
482
|
-
syncHandleLastAccess.delete(path);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
// Reschedule if there are still handles
|
|
486
|
-
if (syncHandleCache.size > 0) {
|
|
487
|
-
scheduleHandleRelease();
|
|
488
|
-
}
|
|
489
|
-
}, UNSAFE_IDLE_TIMEOUT);
|
|
490
|
-
} else {
|
|
491
|
-
// Legacy readwrite: release all handles after 100ms idle
|
|
492
|
-
releaseTimer = setTimeout(() => {
|
|
493
|
-
releaseTimer = null;
|
|
494
|
-
if (syncHandleCache.size > 0) {
|
|
495
|
-
for (const [path, h] of syncHandleCache) {
|
|
496
|
-
// Skip handles that are currently in use
|
|
497
|
-
if (isHandleInUse(path)) continue;
|
|
498
|
-
try { h.flush(); h.close(); } catch {}
|
|
499
|
-
syncHandleCache.delete(path);
|
|
500
|
-
syncHandleLastAccess.delete(path);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
}, LEGACY_RELEASE_DELAY);
|
|
504
|
-
}
|
|
449
|
+
if (unsafeModeSupported) return; // No release needed for readwrite-unsafe
|
|
450
|
+
if (releaseTimer) return; // Already scheduled
|
|
451
|
+
releaseTimer = setTimeout(() => {
|
|
452
|
+
releaseTimer = null;
|
|
453
|
+
const count = syncHandleCache.size;
|
|
454
|
+
for (const h of syncHandleCache.values()) {
|
|
455
|
+
try { h.flush(); h.close(); } catch {}
|
|
456
|
+
}
|
|
457
|
+
syncHandleCache.clear();
|
|
458
|
+
trace('Released ' + count + ' handles (legacy mode debounce)');
|
|
459
|
+
}, LEGACY_RELEASE_DELAY);
|
|
505
460
|
}
|
|
506
461
|
|
|
507
462
|
async function getSyncHandle(filePath, create) {
|
|
508
463
|
const cached = syncHandleCache.get(filePath);
|
|
509
464
|
if (cached) {
|
|
510
|
-
|
|
511
|
-
syncHandleLastAccess.set(filePath, Date.now());
|
|
465
|
+
trace('Handle cache HIT: ' + filePath);
|
|
512
466
|
return cached;
|
|
513
467
|
}
|
|
514
468
|
|
|
515
469
|
// Evict oldest handles if cache is full
|
|
516
470
|
if (syncHandleCache.size >= MAX_HANDLES) {
|
|
517
471
|
const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
|
|
472
|
+
trace('LRU evicting ' + keys.length + ' handles');
|
|
518
473
|
for (const key of keys) {
|
|
519
474
|
const h = syncHandleCache.get(key);
|
|
520
|
-
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key);
|
|
475
|
+
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
|
|
521
476
|
}
|
|
522
477
|
}
|
|
523
478
|
|
|
524
479
|
const fh = await getFileHandle(filePath, create);
|
|
525
480
|
|
|
526
481
|
// Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
|
|
527
|
-
// Falls back to readwrite if not supported
|
|
528
482
|
let access;
|
|
529
483
|
if (unsafeModeSupported === null) {
|
|
530
484
|
// First time - detect support
|
|
531
485
|
try {
|
|
532
486
|
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
533
487
|
unsafeModeSupported = true;
|
|
488
|
+
trace('readwrite-unsafe mode SUPPORTED - handles won\\'t block');
|
|
534
489
|
} catch {
|
|
535
490
|
// Not supported, use default mode
|
|
536
491
|
access = await fh.createSyncAccessHandle();
|
|
537
492
|
unsafeModeSupported = false;
|
|
493
|
+
trace('readwrite-unsafe mode NOT supported - using legacy mode');
|
|
538
494
|
}
|
|
539
495
|
} else if (unsafeModeSupported) {
|
|
540
496
|
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
@@ -543,13 +499,17 @@ async function getSyncHandle(filePath, create) {
|
|
|
543
499
|
}
|
|
544
500
|
|
|
545
501
|
syncHandleCache.set(filePath, access);
|
|
546
|
-
|
|
502
|
+
trace('Handle ACQUIRED: ' + filePath + ' (cache size: ' + syncHandleCache.size + ')');
|
|
547
503
|
return access;
|
|
548
504
|
}
|
|
549
505
|
|
|
550
506
|
function closeSyncHandle(filePath) {
|
|
551
507
|
const h = syncHandleCache.get(filePath);
|
|
552
|
-
if (h) {
|
|
508
|
+
if (h) {
|
|
509
|
+
try { h.close(); } catch {}
|
|
510
|
+
syncHandleCache.delete(filePath);
|
|
511
|
+
trace('Handle RELEASED: ' + filePath);
|
|
512
|
+
}
|
|
553
513
|
}
|
|
554
514
|
|
|
555
515
|
function closeHandlesUnder(prefix) {
|
|
@@ -557,7 +517,6 @@ function closeHandlesUnder(prefix) {
|
|
|
557
517
|
if (p === prefix || p.startsWith(prefix + '/')) {
|
|
558
518
|
try { h.close(); } catch {}
|
|
559
519
|
syncHandleCache.delete(p);
|
|
560
|
-
syncHandleLastAccess.delete(p);
|
|
561
520
|
}
|
|
562
521
|
}
|
|
563
522
|
}
|
|
@@ -632,17 +591,12 @@ async function getParentAndName(filePath) {
|
|
|
632
591
|
|
|
633
592
|
async function handleRead(filePath, payload) {
|
|
634
593
|
const access = await getSyncHandle(filePath, false);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
const bytesRead = access.read(buf, { at: offset });
|
|
642
|
-
return { data: buf.slice(0, bytesRead) };
|
|
643
|
-
} finally {
|
|
644
|
-
endHandleOp(filePath);
|
|
645
|
-
}
|
|
594
|
+
const size = access.getSize();
|
|
595
|
+
const offset = payload?.offset || 0;
|
|
596
|
+
const len = payload?.len || (size - offset);
|
|
597
|
+
const buf = new Uint8Array(len);
|
|
598
|
+
const bytesRead = access.read(buf, { at: offset });
|
|
599
|
+
return { data: buf.slice(0, bytesRead) };
|
|
646
600
|
}
|
|
647
601
|
|
|
648
602
|
// Non-blocking read using getFile() - does NOT lock the file
|
|
@@ -678,57 +632,39 @@ function handleReleaseHandle(filePath) {
|
|
|
678
632
|
|
|
679
633
|
// Force release ALL file handles - use before HMR notifications
|
|
680
634
|
function handleReleaseAllHandles() {
|
|
681
|
-
for (const
|
|
635
|
+
for (const h of syncHandleCache.values()) {
|
|
682
636
|
try { h.close(); } catch {}
|
|
683
637
|
}
|
|
684
638
|
syncHandleCache.clear();
|
|
685
|
-
syncHandleLastAccess.clear();
|
|
686
|
-
syncHandleActiveOps.clear();
|
|
687
639
|
return { success: true };
|
|
688
640
|
}
|
|
689
641
|
|
|
690
642
|
async function handleWrite(filePath, payload) {
|
|
691
643
|
const access = await getSyncHandle(filePath, true);
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
if (
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
access.write(payload.data, { at: offset });
|
|
698
|
-
// Only flush if explicitly requested (default: true for safety)
|
|
699
|
-
if (payload?.flush !== false) access.flush();
|
|
700
|
-
}
|
|
701
|
-
return { success: true };
|
|
702
|
-
} finally {
|
|
703
|
-
endHandleOp(filePath);
|
|
644
|
+
if (payload?.data) {
|
|
645
|
+
const offset = payload.offset ?? 0;
|
|
646
|
+
if (offset === 0) access.truncate(0);
|
|
647
|
+
access.write(payload.data, { at: offset });
|
|
648
|
+
if (payload?.flush !== false) access.flush();
|
|
704
649
|
}
|
|
650
|
+
return { success: true };
|
|
705
651
|
}
|
|
706
652
|
|
|
707
653
|
async function handleAppend(filePath, payload) {
|
|
708
654
|
const access = await getSyncHandle(filePath, true);
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
access.write(payload.data, { at: size });
|
|
714
|
-
if (payload?.flush !== false) access.flush();
|
|
715
|
-
}
|
|
716
|
-
return { success: true };
|
|
717
|
-
} finally {
|
|
718
|
-
endHandleOp(filePath);
|
|
655
|
+
if (payload?.data) {
|
|
656
|
+
const size = access.getSize();
|
|
657
|
+
access.write(payload.data, { at: size });
|
|
658
|
+
if (payload?.flush !== false) access.flush();
|
|
719
659
|
}
|
|
660
|
+
return { success: true };
|
|
720
661
|
}
|
|
721
662
|
|
|
722
663
|
async function handleTruncate(filePath, payload) {
|
|
723
664
|
const access = await getSyncHandle(filePath, false);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
access.flush();
|
|
728
|
-
return { success: true };
|
|
729
|
-
} finally {
|
|
730
|
-
endHandleOp(filePath);
|
|
731
|
-
}
|
|
665
|
+
access.truncate(payload?.len ?? 0);
|
|
666
|
+
access.flush();
|
|
667
|
+
return { success: true };
|
|
732
668
|
}
|
|
733
669
|
|
|
734
670
|
async function handleStat(filePath) {
|
|
@@ -897,18 +833,14 @@ function handleFlush() {
|
|
|
897
833
|
}
|
|
898
834
|
|
|
899
835
|
function handlePurge() {
|
|
900
|
-
// Cancel any pending release timer
|
|
901
836
|
if (releaseTimer) {
|
|
902
837
|
clearTimeout(releaseTimer);
|
|
903
838
|
releaseTimer = null;
|
|
904
839
|
}
|
|
905
|
-
|
|
906
|
-
for (const [, handle] of syncHandleCache) {
|
|
840
|
+
for (const handle of syncHandleCache.values()) {
|
|
907
841
|
try { handle.flush(); handle.close(); } catch {}
|
|
908
842
|
}
|
|
909
843
|
syncHandleCache.clear();
|
|
910
|
-
syncHandleLastAccess.clear();
|
|
911
|
-
syncHandleActiveOps.clear();
|
|
912
844
|
dirCache.clear();
|
|
913
845
|
cachedRoot = null;
|
|
914
846
|
return { success: true };
|
|
@@ -934,6 +866,10 @@ async function processMessage(msg) {
|
|
|
934
866
|
case 'purge': return handlePurge();
|
|
935
867
|
case 'releaseHandle': return handleReleaseHandle(path);
|
|
936
868
|
case 'releaseAllHandles': return handleReleaseAllHandles();
|
|
869
|
+
case 'setDebug':
|
|
870
|
+
debugTrace = !!payload?.enabled;
|
|
871
|
+
trace('Debug tracing ' + (debugTrace ? 'ENABLED' : 'DISABLED') + ', unsafeMode: ' + unsafeModeSupported);
|
|
872
|
+
return { success: true, debugTrace, unsafeModeSupported, cacheSize: syncHandleCache.size };
|
|
937
873
|
default: throw new Error('Unknown operation: ' + type);
|
|
938
874
|
}
|
|
939
875
|
}
|
|
@@ -1790,6 +1726,24 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
1790
1726
|
this.syncCall("purge", "/");
|
|
1791
1727
|
this.statCache.clear();
|
|
1792
1728
|
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Enable or disable debug tracing for handle operations.
|
|
1731
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
1732
|
+
* @param enabled - Whether to enable debug tracing
|
|
1733
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
1734
|
+
*/
|
|
1735
|
+
setDebugSync(enabled) {
|
|
1736
|
+
return this.syncCall("setDebug", "/", { enabled });
|
|
1737
|
+
}
|
|
1738
|
+
/**
|
|
1739
|
+
* Enable or disable debug tracing for handle operations (async version).
|
|
1740
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
1741
|
+
* @param enabled - Whether to enable debug tracing
|
|
1742
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
1743
|
+
*/
|
|
1744
|
+
async setDebug(enabled) {
|
|
1745
|
+
return this.asyncCall("setDebug", "/", { enabled });
|
|
1746
|
+
}
|
|
1793
1747
|
accessSync(filePath, _mode) {
|
|
1794
1748
|
const exists = this.existsSync(filePath);
|
|
1795
1749
|
if (!exists) {
|