Commit fcb1aa83f64 for nodejs

commit fcb1aa83f642290dd29f2876ceca0bc52bf90782
Author: Trivikram Kamat <trivikr.dev@gmail.com>
Date:   Thu Oct 22 15:27:46 2026 -0700

    vfs: fix promises.watch() with an already-aborted signal

    fs.promises.watch() over a virtual mount resolved the first next()
    with { done: true } when the signal was already aborted, while native
    fs.promises.watch() rejects with ABORT_ERR.

    Buffer the AbortError in VFSWatchAsyncIterable so the first next()
    rejects once and later calls behave like an exhausted iterator,
    matching the native async generator. Concurrently pending next()
    calls now resolve after the rejecting one instead of all rejecting.

    Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com>
    Assisted-by: opencode
    PR-URL: https://github.com/nodejs/node/pull/66254
    Fixes: https://github.com/nodejs/node/issues/66253
    Reviewed-By: Matteo Collina <matteo.collina@gmail.com>

diff --git a/lib/internal/vfs/watcher.js b/lib/internal/vfs/watcher.js
index 0f23c1588b1..f6035abfd8b 100644
--- a/lib/internal/vfs/watcher.js
+++ b/lib/internal/vfs/watcher.js
@@ -4,6 +4,7 @@ const {
   ArrayPrototypePush,
   ObjectAssign,
   Promise,
+  PromiseReject,
   PromiseResolve,
   SafeMap,
   SafeSet,
@@ -579,6 +580,7 @@ const kMaxPendingEvents = 1024;
 class VFSWatchAsyncIterable {
   #watcher;
   #closed = false;
+  #abortError = null;
   #pendingEvents = [];
   #pendingResolvers = [];

@@ -616,14 +618,21 @@ class VFSWatchAsyncIterable {
       }
     });

-    // Handle abort signal - reject pending next() with AbortError
+    // Handle abort signal - reject the next pending next() with AbortError,
+    // then behave like an exhausted iterator, matching native fs.promises.watch.
     if (signal) {
       const onAbort = () => {
         this.#closed = true;
         const err = new AbortError(undefined, { cause: signal.reason });
-        while (this.#pendingResolvers.length > 0) {
+        if (this.#pendingResolvers.length > 0) {
           const { reject } = this.#pendingResolvers.shift();
           reject(err);
+          while (this.#pendingResolvers.length > 0) {
+            const { resolve } = this.#pendingResolvers.shift();
+            resolve({ done: true, value: undefined });
+          }
+        } else {
+          this.#abortError = err;
         }
         this.#watcher.close();
       };
@@ -648,6 +657,12 @@ class VFSWatchAsyncIterable {
    * @returns {Promise<IteratorResult>}
    */
   next() {
+    if (this.#abortError !== null) {
+      const err = this.#abortError;
+      this.#abortError = null;
+      return PromiseReject(err);
+    }
+
     if (this.#closed) {
       return PromiseResolve({ done: true, value: undefined });
     }
diff --git a/test/parallel/test-vfs-watch-abort-signal.js b/test/parallel/test-vfs-watch-abort-signal.js
index d335c33caa0..9eb88a4aab9 100644
--- a/test/parallel/test-vfs-watch-abort-signal.js
+++ b/test/parallel/test-vfs-watch-abort-signal.js
@@ -28,11 +28,13 @@ const vfs = require('node:vfs');
     setImmediate(() => myVfs.writeFileSync('/file.txt', 'b'));
   }

-  // promises.watch with pre-aborted signal resolves done immediately
+  // promises.watch with pre-aborted signal rejects the first next() with
+  // AbortError, matching native fs.promises.watch
   {
     const myVfs = vfs.create();
     myVfs.writeFileSync('/p.txt', 'a');
     const iter = myVfs.promises.watch('/p.txt', { signal: AbortSignal.abort() });
+    await assert.rejects(iter.next(), { name: 'AbortError', code: 'ABORT_ERR' });
     const r = await iter.next();
     assert.strictEqual(r.done, true);
   }