@componentor/fs 2.0.7 → 2.0.9
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 +94 -127
- 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 +94 -127
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +69 -111
- 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,75 @@ 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
|
-
|
|
438
|
+
// Debug tracing - set via 'setDebug' message
|
|
439
|
+
let debugTrace = false;
|
|
440
|
+
function trace(...args) {
|
|
441
|
+
if (debugTrace) console.log('[OPFS-T2]', ...args);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Handle release timing
|
|
445
|
+
// - Legacy mode (readwrite): 100ms delay (handles block, release ASAP)
|
|
446
|
+
// - Unsafe mode (readwrite-unsafe): 500ms delay (handles don't block each other,
|
|
447
|
+
// but DO block external tools using default mode like OPFS Explorer)
|
|
443
448
|
let releaseTimer = null;
|
|
444
|
-
const UNSAFE_IDLE_TIMEOUT = 30000;
|
|
445
449
|
const LEGACY_RELEASE_DELAY = 100;
|
|
450
|
+
const UNSAFE_RELEASE_DELAY = 500;
|
|
446
451
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
syncHandleActiveOps.set(path, (syncHandleActiveOps.get(path) || 0) + 1);
|
|
450
|
-
}
|
|
452
|
+
function scheduleHandleRelease() {
|
|
453
|
+
if (releaseTimer) return; // Already scheduled
|
|
451
454
|
|
|
452
|
-
|
|
453
|
-
const count = syncHandleActiveOps.get(path) || 0;
|
|
454
|
-
if (count <= 1) {
|
|
455
|
-
syncHandleActiveOps.delete(path);
|
|
456
|
-
} else {
|
|
457
|
-
syncHandleActiveOps.set(path, count - 1);
|
|
458
|
-
}
|
|
459
|
-
}
|
|
455
|
+
const delay = unsafeModeSupported ? UNSAFE_RELEASE_DELAY : LEGACY_RELEASE_DELAY;
|
|
460
456
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
function scheduleHandleRelease() {
|
|
466
|
-
if (releaseTimer) {
|
|
467
|
-
clearTimeout(releaseTimer);
|
|
468
|
-
}
|
|
457
|
+
releaseTimer = setTimeout(() => {
|
|
458
|
+
releaseTimer = null;
|
|
459
|
+
const count = syncHandleCache.size;
|
|
460
|
+
if (count === 0) return;
|
|
469
461
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
// Skip handles that are currently in use
|
|
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
|
-
}
|
|
462
|
+
for (const h of syncHandleCache.values()) {
|
|
463
|
+
try { h.flush(); h.close(); } catch {}
|
|
464
|
+
}
|
|
465
|
+
syncHandleCache.clear();
|
|
466
|
+
trace('Released ' + count + ' handles (' + (unsafeModeSupported ? 'unsafe' : 'legacy') + ' mode, ' + delay + 'ms delay)');
|
|
467
|
+
}, delay);
|
|
505
468
|
}
|
|
506
469
|
|
|
507
470
|
async function getSyncHandle(filePath, create) {
|
|
508
471
|
const cached = syncHandleCache.get(filePath);
|
|
509
472
|
if (cached) {
|
|
510
|
-
|
|
511
|
-
syncHandleLastAccess.set(filePath, Date.now());
|
|
473
|
+
trace('Handle cache HIT: ' + filePath);
|
|
512
474
|
return cached;
|
|
513
475
|
}
|
|
514
476
|
|
|
515
477
|
// Evict oldest handles if cache is full
|
|
516
478
|
if (syncHandleCache.size >= MAX_HANDLES) {
|
|
517
479
|
const keys = Array.from(syncHandleCache.keys()).slice(0, 10);
|
|
480
|
+
trace('LRU evicting ' + keys.length + ' handles');
|
|
518
481
|
for (const key of keys) {
|
|
519
482
|
const h = syncHandleCache.get(key);
|
|
520
|
-
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key);
|
|
483
|
+
if (h) { try { h.close(); } catch {} syncHandleCache.delete(key); }
|
|
521
484
|
}
|
|
522
485
|
}
|
|
523
486
|
|
|
524
487
|
const fh = await getFileHandle(filePath, create);
|
|
525
488
|
|
|
526
489
|
// Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
|
|
527
|
-
// Falls back to readwrite if not supported
|
|
528
490
|
let access;
|
|
529
491
|
if (unsafeModeSupported === null) {
|
|
530
492
|
// First time - detect support
|
|
531
493
|
try {
|
|
532
494
|
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
533
495
|
unsafeModeSupported = true;
|
|
496
|
+
trace('readwrite-unsafe mode SUPPORTED - handles won\\'t block');
|
|
534
497
|
} catch {
|
|
535
498
|
// Not supported, use default mode
|
|
536
499
|
access = await fh.createSyncAccessHandle();
|
|
537
500
|
unsafeModeSupported = false;
|
|
501
|
+
trace('readwrite-unsafe mode NOT supported - using legacy mode');
|
|
538
502
|
}
|
|
539
503
|
} else if (unsafeModeSupported) {
|
|
540
504
|
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
@@ -543,13 +507,17 @@ async function getSyncHandle(filePath, create) {
|
|
|
543
507
|
}
|
|
544
508
|
|
|
545
509
|
syncHandleCache.set(filePath, access);
|
|
546
|
-
|
|
510
|
+
trace('Handle ACQUIRED: ' + filePath + ' (cache size: ' + syncHandleCache.size + ')');
|
|
547
511
|
return access;
|
|
548
512
|
}
|
|
549
513
|
|
|
550
514
|
function closeSyncHandle(filePath) {
|
|
551
515
|
const h = syncHandleCache.get(filePath);
|
|
552
|
-
if (h) {
|
|
516
|
+
if (h) {
|
|
517
|
+
try { h.close(); } catch {}
|
|
518
|
+
syncHandleCache.delete(filePath);
|
|
519
|
+
trace('Handle RELEASED: ' + filePath);
|
|
520
|
+
}
|
|
553
521
|
}
|
|
554
522
|
|
|
555
523
|
function closeHandlesUnder(prefix) {
|
|
@@ -557,7 +525,6 @@ function closeHandlesUnder(prefix) {
|
|
|
557
525
|
if (p === prefix || p.startsWith(prefix + '/')) {
|
|
558
526
|
try { h.close(); } catch {}
|
|
559
527
|
syncHandleCache.delete(p);
|
|
560
|
-
syncHandleLastAccess.delete(p);
|
|
561
528
|
}
|
|
562
529
|
}
|
|
563
530
|
}
|
|
@@ -632,17 +599,12 @@ async function getParentAndName(filePath) {
|
|
|
632
599
|
|
|
633
600
|
async function handleRead(filePath, payload) {
|
|
634
601
|
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
|
-
}
|
|
602
|
+
const size = access.getSize();
|
|
603
|
+
const offset = payload?.offset || 0;
|
|
604
|
+
const len = payload?.len || (size - offset);
|
|
605
|
+
const buf = new Uint8Array(len);
|
|
606
|
+
const bytesRead = access.read(buf, { at: offset });
|
|
607
|
+
return { data: buf.slice(0, bytesRead) };
|
|
646
608
|
}
|
|
647
609
|
|
|
648
610
|
// Non-blocking read using getFile() - does NOT lock the file
|
|
@@ -678,57 +640,39 @@ function handleReleaseHandle(filePath) {
|
|
|
678
640
|
|
|
679
641
|
// Force release ALL file handles - use before HMR notifications
|
|
680
642
|
function handleReleaseAllHandles() {
|
|
681
|
-
for (const
|
|
643
|
+
for (const h of syncHandleCache.values()) {
|
|
682
644
|
try { h.close(); } catch {}
|
|
683
645
|
}
|
|
684
646
|
syncHandleCache.clear();
|
|
685
|
-
syncHandleLastAccess.clear();
|
|
686
|
-
syncHandleActiveOps.clear();
|
|
687
647
|
return { success: true };
|
|
688
648
|
}
|
|
689
649
|
|
|
690
650
|
async function handleWrite(filePath, payload) {
|
|
691
651
|
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);
|
|
652
|
+
if (payload?.data) {
|
|
653
|
+
const offset = payload.offset ?? 0;
|
|
654
|
+
if (offset === 0) access.truncate(0);
|
|
655
|
+
access.write(payload.data, { at: offset });
|
|
656
|
+
if (payload?.flush !== false) access.flush();
|
|
704
657
|
}
|
|
658
|
+
return { success: true };
|
|
705
659
|
}
|
|
706
660
|
|
|
707
661
|
async function handleAppend(filePath, payload) {
|
|
708
662
|
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);
|
|
663
|
+
if (payload?.data) {
|
|
664
|
+
const size = access.getSize();
|
|
665
|
+
access.write(payload.data, { at: size });
|
|
666
|
+
if (payload?.flush !== false) access.flush();
|
|
719
667
|
}
|
|
668
|
+
return { success: true };
|
|
720
669
|
}
|
|
721
670
|
|
|
722
671
|
async function handleTruncate(filePath, payload) {
|
|
723
672
|
const access = await getSyncHandle(filePath, false);
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
access.flush();
|
|
728
|
-
return { success: true };
|
|
729
|
-
} finally {
|
|
730
|
-
endHandleOp(filePath);
|
|
731
|
-
}
|
|
673
|
+
access.truncate(payload?.len ?? 0);
|
|
674
|
+
access.flush();
|
|
675
|
+
return { success: true };
|
|
732
676
|
}
|
|
733
677
|
|
|
734
678
|
async function handleStat(filePath) {
|
|
@@ -897,18 +841,14 @@ function handleFlush() {
|
|
|
897
841
|
}
|
|
898
842
|
|
|
899
843
|
function handlePurge() {
|
|
900
|
-
// Cancel any pending release timer
|
|
901
844
|
if (releaseTimer) {
|
|
902
845
|
clearTimeout(releaseTimer);
|
|
903
846
|
releaseTimer = null;
|
|
904
847
|
}
|
|
905
|
-
|
|
906
|
-
for (const [, handle] of syncHandleCache) {
|
|
848
|
+
for (const handle of syncHandleCache.values()) {
|
|
907
849
|
try { handle.flush(); handle.close(); } catch {}
|
|
908
850
|
}
|
|
909
851
|
syncHandleCache.clear();
|
|
910
|
-
syncHandleLastAccess.clear();
|
|
911
|
-
syncHandleActiveOps.clear();
|
|
912
852
|
dirCache.clear();
|
|
913
853
|
cachedRoot = null;
|
|
914
854
|
return { success: true };
|
|
@@ -934,6 +874,10 @@ async function processMessage(msg) {
|
|
|
934
874
|
case 'purge': return handlePurge();
|
|
935
875
|
case 'releaseHandle': return handleReleaseHandle(path);
|
|
936
876
|
case 'releaseAllHandles': return handleReleaseAllHandles();
|
|
877
|
+
case 'setDebug':
|
|
878
|
+
debugTrace = !!payload?.enabled;
|
|
879
|
+
trace('Debug tracing ' + (debugTrace ? 'ENABLED' : 'DISABLED') + ', unsafeMode: ' + unsafeModeSupported);
|
|
880
|
+
return { success: true, debugTrace, unsafeModeSupported, cacheSize: syncHandleCache.size };
|
|
937
881
|
default: throw new Error('Unknown operation: ' + type);
|
|
938
882
|
}
|
|
939
883
|
}
|
|
@@ -1002,22 +946,27 @@ async function handleMessage(msg) {
|
|
|
1002
946
|
}
|
|
1003
947
|
}
|
|
1004
948
|
|
|
1005
|
-
// Process queued messages
|
|
1006
|
-
|
|
1007
|
-
|
|
949
|
+
// Process queued messages with concurrency limit
|
|
950
|
+
// Allows multiple operations to run in parallel but prevents overwhelming the worker
|
|
951
|
+
const MAX_CONCURRENT = 8;
|
|
952
|
+
let activeOperations = 0;
|
|
953
|
+
|
|
954
|
+
async function processQueue() {
|
|
955
|
+
while (messageQueue.length > 0 && activeOperations < MAX_CONCURRENT) {
|
|
1008
956
|
const msg = messageQueue.shift();
|
|
1009
|
-
|
|
957
|
+
activeOperations++;
|
|
958
|
+
handleMessage(msg).finally(() => {
|
|
959
|
+
activeOperations--;
|
|
960
|
+
processQueue(); // Process next queued message
|
|
961
|
+
});
|
|
1010
962
|
}
|
|
1011
963
|
}
|
|
1012
964
|
|
|
1013
|
-
//
|
|
1014
|
-
// - Tier 2: Client awaits response before sending next message
|
|
1015
|
-
// - Each OPFSFileSystem instance has its own worker
|
|
965
|
+
// Queue messages and process with controlled concurrency
|
|
1016
966
|
self.onmessage = (event) => {
|
|
967
|
+
messageQueue.push(event.data);
|
|
1017
968
|
if (isReady) {
|
|
1018
|
-
|
|
1019
|
-
} else {
|
|
1020
|
-
messageQueue.push(event.data);
|
|
969
|
+
processQueue();
|
|
1021
970
|
}
|
|
1022
971
|
};
|
|
1023
972
|
|
|
@@ -1790,6 +1739,24 @@ var OPFSFileSystem = class _OPFSFileSystem {
|
|
|
1790
1739
|
this.syncCall("purge", "/");
|
|
1791
1740
|
this.statCache.clear();
|
|
1792
1741
|
}
|
|
1742
|
+
/**
|
|
1743
|
+
* Enable or disable debug tracing for handle operations.
|
|
1744
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
1745
|
+
* @param enabled - Whether to enable debug tracing
|
|
1746
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
1747
|
+
*/
|
|
1748
|
+
setDebugSync(enabled) {
|
|
1749
|
+
return this.syncCall("setDebug", "/", { enabled });
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Enable or disable debug tracing for handle operations (async version).
|
|
1753
|
+
* When enabled, logs handle cache hits, acquisitions, releases, and mode information.
|
|
1754
|
+
* @param enabled - Whether to enable debug tracing
|
|
1755
|
+
* @returns Debug state information including unsafeModeSupported and cache size
|
|
1756
|
+
*/
|
|
1757
|
+
async setDebug(enabled) {
|
|
1758
|
+
return this.asyncCall("setDebug", "/", { enabled });
|
|
1759
|
+
}
|
|
1793
1760
|
accessSync(filePath, _mode) {
|
|
1794
1761
|
const exists = this.existsSync(filePath);
|
|
1795
1762
|
if (!exists) {
|