From ebc1f19526310c93a2536c674b97780d36602d97 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:17:15 -0700 Subject: [PATCH] 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 --- lib/internal/vfs/watcher.js | 19 +++++++++++++++++-- test/parallel/test-vfs-watch-abort-signal.js | 4 +++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/lib/internal/vfs/watcher.js b/lib/internal/vfs/watcher.js index 0f23c1588b17..f6035abfd8be 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} */ 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 4ceb9f787445..2b62ffe74788 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); }