@componentor/fs 3.0.51 → 3.0.53

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/dist/index.d.mts CHANGED
@@ -348,6 +348,12 @@ declare class VFSFileSystem {
348
348
  private rejectReady;
349
349
  private initError;
350
350
  private isReady;
351
+ /** True while a leader transition is in flight (promotion to leader, etc.).
352
+ * Cleared the moment the new sync-relay signals `ready`. Consumers can
353
+ * combine this with `isReady` to know when sync FS ops are safe again. */
354
+ private transitioning;
355
+ /** Listeners awaiting the next `ready` signal (used by `whenReady()`). */
356
+ private readyListeners;
351
357
  private config;
352
358
  private tabId;
353
359
  private _mode;
@@ -514,6 +520,23 @@ declare class VFSFileSystem {
514
520
  * Rejects with corruption error if VFS was corrupt (but system falls back to OPFS mode).
515
521
  * Callers can catch and continue — the fs API works in OPFS mode after rejection. */
516
522
  init(): Promise<void>;
523
+ /** True only while the filesystem is fully ready for synchronous operations
524
+ * AND no leader transition is in progress. Reflects the moment-in-time state;
525
+ * use `whenReady()` to await readiness reliably. */
526
+ get ready(): boolean;
527
+ /** Resolves once the filesystem is fully ready for synchronous operations,
528
+ * including any in-flight leader transition (promotion-to-leader, etc.).
529
+ * If already ready and no transition is pending, resolves immediately.
530
+ *
531
+ * Use this when coordinating with other Web-Lock-based systems (e.g. a
532
+ * parent app that elects its own leader independently of the FS) — the
533
+ * timing of the two elections isn't synchronized, so the FS may still be
534
+ * reinitialising when the parent's lock fires. Calling `whenReady()`
535
+ * after your own leader-acquisition guarantees the FS is back in a state
536
+ * where sync ops won't stall the 20-second relay-worker heartbeat. */
537
+ whenReady(): Promise<void>;
538
+ /** Internal — called by lifecycle handlers when sync-relay says 'ready'. */
539
+ private fireReadyListeners;
517
540
  /** Switch the filesystem mode at runtime.
518
541
  *
519
542
  * Typical flow for IDE corruption recovery:
package/dist/index.js CHANGED
@@ -362,7 +362,12 @@ var OP = {
362
362
  var SAB_OFFSETS = {
363
363
  // Int32 - bytes in this chunk
364
364
  TOTAL_LEN: 16,
365
- // Int32 - reserved
365
+ // Int32 - 0-based chunk index
366
+ HEARTBEAT: 28,
367
+ // Int32 - liveness counter; the relay worker bumps this ~1×/s
368
+ // while its event loop is alive (incl. mid-await of a
369
+ // long op) so a spin-waiting main thread can tell
370
+ // "slow" from "dead". Never written by the main thread.
366
371
  HEADER_SIZE: 32
367
372
  // Data payload starts here
368
373
  };
@@ -2556,19 +2561,37 @@ var DEFAULT_SAB_SIZE = 2 * 1024 * 1024;
2556
2561
  var instanceRegistry = /* @__PURE__ */ new Map();
2557
2562
  var HEADER_SIZE = SAB_OFFSETS.HEADER_SIZE;
2558
2563
  var _canAtomicsWait = typeof globalThis.WorkerGlobalScope !== "undefined";
2559
- var SPIN_TIMEOUT_MS = 1e4;
2560
- function spinWait(arr, index, value) {
2564
+ var SAB_HEARTBEAT_INDEX = SAB_OFFSETS.HEARTBEAT >> 2;
2565
+ var SPIN_STALL_TIMEOUT_MS = 2e4;
2566
+ var SPIN_NO_HEARTBEAT_TIMEOUT_MS = 3e4;
2567
+ function spinWait(arr, index, value, heartbeatArr) {
2561
2568
  if (_canAtomicsWait) {
2562
2569
  Atomics.wait(arr, index, value);
2563
- } else {
2570
+ return;
2571
+ }
2572
+ if (!heartbeatArr) {
2564
2573
  const start = performance.now();
2565
2574
  while (Atomics.load(arr, index) === value) {
2566
- if (performance.now() - start > SPIN_TIMEOUT_MS) {
2575
+ if (performance.now() - start > SPIN_NO_HEARTBEAT_TIMEOUT_MS) {
2567
2576
  throw new Error(
2568
- `VFS sync operation timed out after ${SPIN_TIMEOUT_MS / 1e3}s \u2014 SharedWorker may be unresponsive`
2577
+ `VFS sync operation timed out after ${SPIN_NO_HEARTBEAT_TIMEOUT_MS / 1e3}s \u2014 relay worker did not respond`
2569
2578
  );
2570
2579
  }
2571
2580
  }
2581
+ return;
2582
+ }
2583
+ let lastBeat = Atomics.load(heartbeatArr, SAB_HEARTBEAT_INDEX);
2584
+ let lastProgress = performance.now();
2585
+ while (Atomics.load(arr, index) === value) {
2586
+ const beat = Atomics.load(heartbeatArr, SAB_HEARTBEAT_INDEX);
2587
+ if (beat !== lastBeat) {
2588
+ lastBeat = beat;
2589
+ lastProgress = performance.now();
2590
+ } else if (performance.now() - lastProgress > SPIN_STALL_TIMEOUT_MS) {
2591
+ throw new Error(
2592
+ `VFS sync operation aborted: relay worker heartbeat stalled for ${SPIN_STALL_TIMEOUT_MS / 1e3}s \u2014 worker is unresponsive`
2593
+ );
2594
+ }
2572
2595
  }
2573
2596
  }
2574
2597
  var VFSFileSystem = class {
@@ -2593,6 +2616,12 @@ var VFSFileSystem = class {
2593
2616
  rejectReady;
2594
2617
  initError = null;
2595
2618
  isReady = false;
2619
+ /** True while a leader transition is in flight (promotion to leader, etc.).
2620
+ * Cleared the moment the new sync-relay signals `ready`. Consumers can
2621
+ * combine this with `isReady` to know when sync FS ops are safe again. */
2622
+ transitioning = false;
2623
+ /** Listeners awaiting the next `ready` signal (used by `whenReady()`). */
2624
+ readyListeners = /* @__PURE__ */ new Set();
2596
2625
  // Config (definite assignment — always set when constructor doesn't return singleton)
2597
2626
  config;
2598
2627
  tabId;
@@ -2668,8 +2697,10 @@ var VFSFileSystem = class {
2668
2697
  const msg = e.data;
2669
2698
  if (msg.type === "ready") {
2670
2699
  this.isReady = true;
2700
+ this.transitioning = false;
2671
2701
  this.initAsyncRelay();
2672
2702
  this.resolveReady();
2703
+ this.fireReadyListeners();
2673
2704
  if (!this.isFollower) {
2674
2705
  this.initLeaderBroker();
2675
2706
  }
@@ -2940,6 +2971,7 @@ var VFSFileSystem = class {
2940
2971
  promoteToLeader() {
2941
2972
  this.isFollower = false;
2942
2973
  this.isReady = false;
2974
+ this.transitioning = true;
2943
2975
  this.brokerInitialized = false;
2944
2976
  if (this.brokerHeartbeatTimer) {
2945
2977
  clearInterval(this.brokerHeartbeatTimer);
@@ -2976,7 +3008,9 @@ var VFSFileSystem = class {
2976
3008
  const msg = e.data;
2977
3009
  if (msg.type === "ready") {
2978
3010
  this.isReady = true;
3011
+ this.transitioning = false;
2979
3012
  this.resolveReady();
3013
+ this.fireReadyListeners();
2980
3014
  this.initLeaderBroker();
2981
3015
  } else if (msg.type === "init-failed") {
2982
3016
  if (msg.error?.startsWith("Corrupt VFS:")) {
@@ -3041,7 +3075,7 @@ var VFSFileSystem = class {
3041
3075
  if (signal === -1) {
3042
3076
  throw this.initError ?? new Error("VFS initialization failed");
3043
3077
  }
3044
- spinWait(this.readySignal, 0, 0);
3078
+ spinWait(this.readySignal, 0, 0, this.ctrl);
3045
3079
  const finalSignal = Atomics.load(this.readySignal, 0);
3046
3080
  if (finalSignal === -1) {
3047
3081
  throw this.initError ?? new Error("VFS initialization failed");
@@ -3055,7 +3089,8 @@ var VFSFileSystem = class {
3055
3089
  const maxChunk = this.sab.byteLength - HEADER_SIZE;
3056
3090
  const requestBytes = new Uint8Array(requestBuf);
3057
3091
  const totalLenView = new BigUint64Array(this.sab, SAB_OFFSETS.TOTAL_LEN, 1);
3058
- if (requestBytes.byteLength <= maxChunk) {
3092
+ const multiChunkRequest = requestBytes.byteLength > maxChunk;
3093
+ if (!multiChunkRequest) {
3059
3094
  new Uint8Array(this.sab, HEADER_SIZE, requestBytes.byteLength).set(requestBytes);
3060
3095
  Atomics.store(this.ctrl, 3, requestBytes.byteLength);
3061
3096
  Atomics.store(totalLenView, 0, BigInt(requestBytes.byteLength));
@@ -3079,11 +3114,11 @@ var VFSFileSystem = class {
3079
3114
  Atomics.notify(this.ctrl, 0);
3080
3115
  sent += chunkSize;
3081
3116
  if (sent < requestBytes.byteLength) {
3082
- spinWait(this.ctrl, 0, sent === chunkSize ? SIGNAL.REQUEST : SIGNAL.CHUNK);
3117
+ spinWait(this.ctrl, 0, sent === chunkSize ? SIGNAL.REQUEST : SIGNAL.CHUNK, this.ctrl);
3083
3118
  }
3084
3119
  }
3085
3120
  }
3086
- spinWait(this.ctrl, 0, SIGNAL.REQUEST);
3121
+ spinWait(this.ctrl, 0, multiChunkRequest ? SIGNAL.CHUNK : SIGNAL.REQUEST, this.ctrl);
3087
3122
  const signal = Atomics.load(this.ctrl, 0);
3088
3123
  const respChunkLen = Atomics.load(this.ctrl, 3);
3089
3124
  const respTotalLen = Number(Atomics.load(totalLenView, 0));
@@ -3099,7 +3134,7 @@ var VFSFileSystem = class {
3099
3134
  while (received < respTotalLen) {
3100
3135
  Atomics.store(this.ctrl, 0, SIGNAL.CHUNK_ACK);
3101
3136
  Atomics.notify(this.ctrl, 0);
3102
- spinWait(this.ctrl, 0, SIGNAL.CHUNK_ACK);
3137
+ spinWait(this.ctrl, 0, SIGNAL.CHUNK_ACK, this.ctrl);
3103
3138
  const nextLen = Atomics.load(this.ctrl, 3);
3104
3139
  responseBytes.set(new Uint8Array(this.sab, HEADER_SIZE, nextLen), received);
3105
3140
  received += nextLen;
@@ -3590,6 +3625,44 @@ var VFSFileSystem = class {
3590
3625
  }
3591
3626
  });
3592
3627
  }
3628
+ /** True only while the filesystem is fully ready for synchronous operations
3629
+ * AND no leader transition is in progress. Reflects the moment-in-time state;
3630
+ * use `whenReady()` to await readiness reliably. */
3631
+ get ready() {
3632
+ return this.isReady && !this.transitioning;
3633
+ }
3634
+ /** Resolves once the filesystem is fully ready for synchronous operations,
3635
+ * including any in-flight leader transition (promotion-to-leader, etc.).
3636
+ * If already ready and no transition is pending, resolves immediately.
3637
+ *
3638
+ * Use this when coordinating with other Web-Lock-based systems (e.g. a
3639
+ * parent app that elects its own leader independently of the FS) — the
3640
+ * timing of the two elections isn't synchronized, so the FS may still be
3641
+ * reinitialising when the parent's lock fires. Calling `whenReady()`
3642
+ * after your own leader-acquisition guarantees the FS is back in a state
3643
+ * where sync ops won't stall the 20-second relay-worker heartbeat. */
3644
+ whenReady() {
3645
+ if (this.isReady && !this.transitioning) return Promise.resolve();
3646
+ if (this.transitioning) {
3647
+ return new Promise((resolve2) => {
3648
+ this.readyListeners.add(resolve2);
3649
+ });
3650
+ }
3651
+ return this.readyPromise.then(() => {
3652
+ });
3653
+ }
3654
+ /** Internal — called by lifecycle handlers when sync-relay says 'ready'. */
3655
+ fireReadyListeners() {
3656
+ const listeners = Array.from(this.readyListeners);
3657
+ this.readyListeners.clear();
3658
+ for (const l of listeners) {
3659
+ try {
3660
+ l();
3661
+ } catch (e) {
3662
+ console.warn("[VFS] readyListener threw:", e);
3663
+ }
3664
+ }
3665
+ }
3593
3666
  /** Switch the filesystem mode at runtime.
3594
3667
  *
3595
3668
  * Typical flow for IDE corruption recovery:
@@ -3627,7 +3700,9 @@ var VFSFileSystem = class {
3627
3700
  const msg = e.data;
3628
3701
  if (msg.type === "ready") {
3629
3702
  this.isReady = true;
3703
+ this.transitioning = false;
3630
3704
  this.resolveReady();
3705
+ this.fireReadyListeners();
3631
3706
  if (!this.isFollower) {
3632
3707
  this.initLeaderBroker();
3633
3708
  }