From 8d1e9bda31672100ae000f8714c4637166950c6e Mon Sep 17 00:00:00 2001 From: Ishaan Kapur <64529428+ishaanlabs-gg@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:02:45 +0530 Subject: [PATCH 1/3] fs: remove FileHandle stream close listener --- lib/internal/fs/streams.js | 28 ++++++- .../test-fs-promises-file-handle-stream.js | 80 +++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/lib/internal/fs/streams.js b/lib/internal/fs/streams.js index c2e8abc4efdadd..33f34d76c51eff 100644 --- a/lib/internal/fs/streams.js +++ b/lib/internal/fs/streams.js @@ -45,6 +45,8 @@ const kIsPerformingIO = Symbol('kIsPerformingIO'); const kFs = Symbol('kFs'); const kHandle = Symbol('kHandle'); +const kHandleCloseListener = Symbol('kHandleCloseListener'); +const kHandleCloseListenerCleanup = Symbol('kHandleCloseListenerCleanup'); function _construct(callback) { const stream = this; @@ -152,7 +154,16 @@ function importFd(stream, options) { stream[kHandle] = options.fd; stream[kFs] = FileHandleOperations(stream[kHandle]); stream[kHandle][kRef](); - options.fd.on('close', FunctionPrototypeBind(stream.close, stream)); + if (options.autoClose === false) { + stream[kHandleCloseListener] = FunctionPrototypeBind(stream.close, stream); + stream[kHandleCloseListenerCleanup] = FunctionPrototypeBind( + cleanupHandleCloseListener, stream); + options.fd.once('close', stream[kHandleCloseListener]); + stream.once('close', stream[kHandleCloseListenerCleanup]); + stream.once('end', stream[kHandleCloseListenerCleanup]); + stream.once('finish', stream[kHandleCloseListenerCleanup]); + stream.once('error', stream[kHandleCloseListenerCleanup]); + } return options.fd.fd; } @@ -160,6 +171,21 @@ function importFd(stream, options) { ['number', 'FileHandle'], options.fd); } +function cleanupHandleCloseListener() { + if (this[kHandleCloseListener] === undefined) { + return; + } + + this[kHandle].removeListener('close', this[kHandleCloseListener]); + this[kHandle][kUnref](); + this.removeListener('close', this[kHandleCloseListenerCleanup]); + this.removeListener('end', this[kHandleCloseListenerCleanup]); + this.removeListener('finish', this[kHandleCloseListenerCleanup]); + this.removeListener('error', this[kHandleCloseListenerCleanup]); + this[kHandleCloseListener] = undefined; + this[kHandleCloseListenerCleanup] = undefined; +} + function ReadStream(path, options) { if (!(this instanceof ReadStream)) return new ReadStream(path, options); diff --git a/test/parallel/test-fs-promises-file-handle-stream.js b/test/parallel/test-fs-promises-file-handle-stream.js index 71f312b6f9d78c..c4f2ae0c9812a6 100644 --- a/test/parallel/test-fs-promises-file-handle-stream.js +++ b/test/parallel/test-fs-promises-file-handle-stream.js @@ -42,7 +42,87 @@ async function validateRead() { ); } +async function validateReadStreamReleasesFileHandleCloseListener() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-read-listener.txt'); + const buf = Buffer.from('Hello world', 'utf8'); + + fs.writeFileSync(filePathForHandle, buf); + + const fileHandle = await open(filePathForHandle); + + for (let i = 0; i < buf.length; i++) { + await buffer(fileHandle.createReadStream({ + start: i, + end: i, + autoClose: false, + })); + + assert.strictEqual(fileHandle.listenerCount('close'), 0); + } + + await fileHandle.close(); +} + +async function validateWriteStreamReleasesFileHandleCloseListener() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-write-listener.txt'); + const buf = Buffer.from('Hello world', 'utf8'); + + const fileHandle = await open(filePathForHandle, 'w'); + + for (let i = 0; i < buf.length; i++) { + const stream = fileHandle.createWriteStream({ + start: i, + autoClose: false, + }); + stream.end(buf.subarray(i, i + 1)); + await finished(stream); + + assert.strictEqual(fileHandle.listenerCount('close'), 0); + } + + await fileHandle.close(); + assert.deepStrictEqual(fs.readFileSync(filePathForHandle), buf); +} + +async function validateReadStreamAutoCloseClosesFileHandle() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-read-auto-close.txt'); + const buf = Buffer.from('Hello world', 'utf8'); + + fs.writeFileSync(filePathForHandle, buf); + + const fileHandle = await open(filePathForHandle); + const closed = new Promise((resolve) => { + fileHandle.once('close', common.mustCall(resolve)); + }); + + assert.deepStrictEqual(await buffer(fileHandle.createReadStream()), buf); + await closed; + assert.strictEqual(fileHandle.listenerCount('close'), 0); +} + +async function validateWriteStreamAutoCloseClosesFileHandle() { + const filePathForHandle = path.resolve(tmpDir, 'tmp-write-auto-close.txt'); + const buf = Buffer.from('Hello world', 'utf8'); + + const fileHandle = await open(filePathForHandle, 'w'); + const closed = new Promise((resolve) => { + fileHandle.once('close', common.mustCall(resolve)); + }); + const stream = fileHandle.createWriteStream(); + + stream.end(buf); + await finished(stream); + await closed; + + assert.strictEqual(fileHandle.listenerCount('close'), 0); + assert.deepStrictEqual(fs.readFileSync(filePathForHandle), buf); +} + Promise.all([ validateWrite(), validateRead(), + validateReadStreamReleasesFileHandleCloseListener(), + validateWriteStreamReleasesFileHandleCloseListener(), + validateReadStreamAutoCloseClosesFileHandle(), + validateWriteStreamAutoCloseClosesFileHandle(), ]).then(common.mustCall()); From 442374e06164ab28cba8675eec7e4459ff8b6dc4 Mon Sep 17 00:00:00 2001 From: Ishaan Kapur <64529428+ishaanlabs-gg@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:02:45 +0530 Subject: [PATCH 2/3] fs: avoid redundant FileHandle stream close listener --- lib/internal/fs/streams.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/internal/fs/streams.js b/lib/internal/fs/streams.js index 33f34d76c51eff..6e3cba83092624 100644 --- a/lib/internal/fs/streams.js +++ b/lib/internal/fs/streams.js @@ -159,7 +159,6 @@ function importFd(stream, options) { stream[kHandleCloseListenerCleanup] = FunctionPrototypeBind( cleanupHandleCloseListener, stream); options.fd.once('close', stream[kHandleCloseListener]); - stream.once('close', stream[kHandleCloseListenerCleanup]); stream.once('end', stream[kHandleCloseListenerCleanup]); stream.once('finish', stream[kHandleCloseListenerCleanup]); stream.once('error', stream[kHandleCloseListenerCleanup]); @@ -178,7 +177,6 @@ function cleanupHandleCloseListener() { this[kHandle].removeListener('close', this[kHandleCloseListener]); this[kHandle][kUnref](); - this.removeListener('close', this[kHandleCloseListenerCleanup]); this.removeListener('end', this[kHandleCloseListenerCleanup]); this.removeListener('finish', this[kHandleCloseListenerCleanup]); this.removeListener('error', this[kHandleCloseListenerCleanup]); From 610d021c884aacc0924c3d8c63c859e8154e704a Mon Sep 17 00:00:00 2001 From: Ishaan Kapur <64529428+ishaanlabs-gg@users.noreply.github.com> Date: Thu, 2 Jul 2026 16:02:46 +0530 Subject: [PATCH 3/3] fs: remove redundant FileHandle close listener --- lib/internal/fs/streams.js | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/lib/internal/fs/streams.js b/lib/internal/fs/streams.js index 6e3cba83092624..20d57ffe84c232 100644 --- a/lib/internal/fs/streams.js +++ b/lib/internal/fs/streams.js @@ -45,8 +45,7 @@ const kIsPerformingIO = Symbol('kIsPerformingIO'); const kFs = Symbol('kFs'); const kHandle = Symbol('kHandle'); -const kHandleCloseListener = Symbol('kHandleCloseListener'); -const kHandleCloseListenerCleanup = Symbol('kHandleCloseListenerCleanup'); +const kHandleCleanup = Symbol('kHandleCleanup'); function _construct(callback) { const stream = this; @@ -155,13 +154,10 @@ function importFd(stream, options) { stream[kFs] = FileHandleOperations(stream[kHandle]); stream[kHandle][kRef](); if (options.autoClose === false) { - stream[kHandleCloseListener] = FunctionPrototypeBind(stream.close, stream); - stream[kHandleCloseListenerCleanup] = FunctionPrototypeBind( - cleanupHandleCloseListener, stream); - options.fd.once('close', stream[kHandleCloseListener]); - stream.once('end', stream[kHandleCloseListenerCleanup]); - stream.once('finish', stream[kHandleCloseListenerCleanup]); - stream.once('error', stream[kHandleCloseListenerCleanup]); + stream[kHandleCleanup] = FunctionPrototypeBind(cleanupFileHandleRef, stream); + stream.once('end', stream[kHandleCleanup]); + stream.once('finish', stream[kHandleCleanup]); + stream.once('error', stream[kHandleCleanup]); } return options.fd.fd; } @@ -170,18 +166,16 @@ function importFd(stream, options) { ['number', 'FileHandle'], options.fd); } -function cleanupHandleCloseListener() { - if (this[kHandleCloseListener] === undefined) { +function cleanupFileHandleRef() { + if (this[kHandleCleanup] === undefined) { return; } - this[kHandle].removeListener('close', this[kHandleCloseListener]); this[kHandle][kUnref](); - this.removeListener('end', this[kHandleCloseListenerCleanup]); - this.removeListener('finish', this[kHandleCloseListenerCleanup]); - this.removeListener('error', this[kHandleCloseListenerCleanup]); - this[kHandleCloseListener] = undefined; - this[kHandleCloseListenerCleanup] = undefined; + this.removeListener('end', this[kHandleCleanup]); + this.removeListener('finish', this[kHandleCleanup]); + this.removeListener('error', this[kHandleCleanup]); + this[kHandleCleanup] = undefined; } function ReadStream(path, options) {