@componentor/fs 2.0.5 → 2.0.7
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 +10 -9
- package/dist/index.cjs +145 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +145 -41
- package/dist/index.js.map +1 -1
- package/dist/kernel.js +119 -46
- package/dist/kernel.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -426,33 +426,88 @@ let isReady = false;
|
|
|
426
426
|
let cachedRoot = null;
|
|
427
427
|
const dirCache = new Map();
|
|
428
428
|
|
|
429
|
-
// Sync handle cache - MAJOR performance optimization
|
|
430
|
-
//
|
|
429
|
+
// Sync handle cache - MAJOR performance optimization (2-5x speedup)
|
|
430
|
+
// Uses readwrite-unsafe mode when available (no exclusive lock, allows external access)
|
|
431
|
+
// Falls back to readwrite with debounced release for older browsers
|
|
431
432
|
const syncHandleCache = new Map();
|
|
432
433
|
const syncHandleLastAccess = new Map();
|
|
434
|
+
const syncHandleActiveOps = new Map(); // Track active operations per handle
|
|
433
435
|
const MAX_HANDLES = 100;
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
436
|
+
|
|
437
|
+
// Track if readwrite-unsafe mode is supported (detected on first use)
|
|
438
|
+
let unsafeModeSupported = null;
|
|
439
|
+
|
|
440
|
+
// Handle release timing:
|
|
441
|
+
// - readwrite-unsafe: 30s idle timeout (no blocking, just memory management)
|
|
442
|
+
// - readwrite (fallback): 100ms debounce (need to release locks quickly)
|
|
443
|
+
let releaseTimer = null;
|
|
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
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function isHandleInUse(path) {
|
|
462
|
+
return (syncHandleActiveOps.get(path) || 0) > 0;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function scheduleHandleRelease() {
|
|
466
|
+
if (releaseTimer) {
|
|
467
|
+
clearTimeout(releaseTimer);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (unsafeModeSupported) {
|
|
471
|
+
// readwrite-unsafe: clean up handles idle for 30s
|
|
472
|
+
releaseTimer = setTimeout(() => {
|
|
473
|
+
releaseTimer = null;
|
|
474
|
+
const now = Date.now();
|
|
475
|
+
for (const [path, lastAccess] of syncHandleLastAccess) {
|
|
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
|
+
}
|
|
447
484
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
+
}
|
|
451
505
|
}
|
|
452
506
|
|
|
453
507
|
async function getSyncHandle(filePath, create) {
|
|
454
508
|
const cached = syncHandleCache.get(filePath);
|
|
455
509
|
if (cached) {
|
|
510
|
+
// Renew last access time
|
|
456
511
|
syncHandleLastAccess.set(filePath, Date.now());
|
|
457
512
|
return cached;
|
|
458
513
|
}
|
|
@@ -467,10 +522,28 @@ async function getSyncHandle(filePath, create) {
|
|
|
467
522
|
}
|
|
468
523
|
|
|
469
524
|
const fh = await getFileHandle(filePath, create);
|
|
470
|
-
|
|
525
|
+
|
|
526
|
+
// Try readwrite-unsafe mode first (no exclusive lock, Chrome 121+)
|
|
527
|
+
// Falls back to readwrite if not supported
|
|
528
|
+
let access;
|
|
529
|
+
if (unsafeModeSupported === null) {
|
|
530
|
+
// First time - detect support
|
|
531
|
+
try {
|
|
532
|
+
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
533
|
+
unsafeModeSupported = true;
|
|
534
|
+
} catch {
|
|
535
|
+
// Not supported, use default mode
|
|
536
|
+
access = await fh.createSyncAccessHandle();
|
|
537
|
+
unsafeModeSupported = false;
|
|
538
|
+
}
|
|
539
|
+
} else if (unsafeModeSupported) {
|
|
540
|
+
access = await fh.createSyncAccessHandle({ mode: 'readwrite-unsafe' });
|
|
541
|
+
} else {
|
|
542
|
+
access = await fh.createSyncAccessHandle();
|
|
543
|
+
}
|
|
544
|
+
|
|
471
545
|
syncHandleCache.set(filePath, access);
|
|
472
546
|
syncHandleLastAccess.set(filePath, Date.now());
|
|
473
|
-
scheduleIdleCleanup();
|
|
474
547
|
return access;
|
|
475
548
|
}
|
|
476
549
|
|
|
@@ -559,12 +632,17 @@ async function getParentAndName(filePath) {
|
|
|
559
632
|
|
|
560
633
|
async function handleRead(filePath, payload) {
|
|
561
634
|
const access = await getSyncHandle(filePath, false);
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
635
|
+
beginHandleOp(filePath);
|
|
636
|
+
try {
|
|
637
|
+
const size = access.getSize();
|
|
638
|
+
const offset = payload?.offset || 0;
|
|
639
|
+
const len = payload?.len || (size - offset);
|
|
640
|
+
const buf = new Uint8Array(len);
|
|
641
|
+
const bytesRead = access.read(buf, { at: offset });
|
|
642
|
+
return { data: buf.slice(0, bytesRead) };
|
|
643
|
+
} finally {
|
|
644
|
+
endHandleOp(filePath);
|
|
645
|
+
}
|
|
568
646
|
}
|
|
569
647
|
|
|
570
648
|
// Non-blocking read using getFile() - does NOT lock the file
|
|
@@ -605,36 +683,52 @@ function handleReleaseAllHandles() {
|
|
|
605
683
|
}
|
|
606
684
|
syncHandleCache.clear();
|
|
607
685
|
syncHandleLastAccess.clear();
|
|
686
|
+
syncHandleActiveOps.clear();
|
|
608
687
|
return { success: true };
|
|
609
688
|
}
|
|
610
689
|
|
|
611
690
|
async function handleWrite(filePath, payload) {
|
|
612
691
|
const access = await getSyncHandle(filePath, true);
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if (
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
692
|
+
beginHandleOp(filePath);
|
|
693
|
+
try {
|
|
694
|
+
if (payload?.data) {
|
|
695
|
+
const offset = payload.offset ?? 0;
|
|
696
|
+
if (offset === 0) access.truncate(0);
|
|
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);
|
|
619
704
|
}
|
|
620
|
-
return { success: true };
|
|
621
705
|
}
|
|
622
706
|
|
|
623
707
|
async function handleAppend(filePath, payload) {
|
|
624
708
|
const access = await getSyncHandle(filePath, true);
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
709
|
+
beginHandleOp(filePath);
|
|
710
|
+
try {
|
|
711
|
+
if (payload?.data) {
|
|
712
|
+
const size = access.getSize();
|
|
713
|
+
access.write(payload.data, { at: size });
|
|
714
|
+
if (payload?.flush !== false) access.flush();
|
|
715
|
+
}
|
|
716
|
+
return { success: true };
|
|
717
|
+
} finally {
|
|
718
|
+
endHandleOp(filePath);
|
|
629
719
|
}
|
|
630
|
-
return { success: true };
|
|
631
720
|
}
|
|
632
721
|
|
|
633
722
|
async function handleTruncate(filePath, payload) {
|
|
634
723
|
const access = await getSyncHandle(filePath, false);
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
724
|
+
beginHandleOp(filePath);
|
|
725
|
+
try {
|
|
726
|
+
access.truncate(payload?.len ?? 0);
|
|
727
|
+
access.flush();
|
|
728
|
+
return { success: true };
|
|
729
|
+
} finally {
|
|
730
|
+
endHandleOp(filePath);
|
|
731
|
+
}
|
|
638
732
|
}
|
|
639
733
|
|
|
640
734
|
async function handleStat(filePath) {
|
|
@@ -803,11 +897,18 @@ function handleFlush() {
|
|
|
803
897
|
}
|
|
804
898
|
|
|
805
899
|
function handlePurge() {
|
|
900
|
+
// Cancel any pending release timer
|
|
901
|
+
if (releaseTimer) {
|
|
902
|
+
clearTimeout(releaseTimer);
|
|
903
|
+
releaseTimer = null;
|
|
904
|
+
}
|
|
806
905
|
// Flush and close all cached sync handles
|
|
807
906
|
for (const [, handle] of syncHandleCache) {
|
|
808
907
|
try { handle.flush(); handle.close(); } catch {}
|
|
809
908
|
}
|
|
810
909
|
syncHandleCache.clear();
|
|
910
|
+
syncHandleLastAccess.clear();
|
|
911
|
+
syncHandleActiveOps.clear();
|
|
811
912
|
dirCache.clear();
|
|
812
913
|
cachedRoot = null;
|
|
813
914
|
return { success: true };
|
|
@@ -895,6 +996,9 @@ async function handleMessage(msg) {
|
|
|
895
996
|
} else {
|
|
896
997
|
self.postMessage({ id, error: errorCode, code: errorCode });
|
|
897
998
|
}
|
|
999
|
+
} finally {
|
|
1000
|
+
// Schedule handle release (debounced - waits 100ms after last operation)
|
|
1001
|
+
scheduleHandleRelease();
|
|
898
1002
|
}
|
|
899
1003
|
}
|
|
900
1004
|
|