From 30c99a7a22f7696148322a02b271041e2f8f6840 Mon Sep 17 00:00:00 2001
From: Aaron Ang <67321817+aaron-ang@users.noreply.github.com>
Date: Tue, 14 Jan 2025 04:01:14 -0500
Subject: [PATCH] fix(ext/node): add `writev` method to `FileHandle` (#27563)

Part of #25554
---
 ext/node/lib.rs                               |  2 +-
 ext/node/polyfills/_fs/_fs_writev.d.ts        | 65 -----------------
 .../_fs/{_fs_writev.mjs => _fs_writev.ts}     | 73 +++++++++++++++++--
 ext/node/polyfills/fs.ts                      |  2 +-
 ext/node/polyfills/internal/fs/handle.ts      | 13 +++-
 ext/node/polyfills/internal/fs/streams.mjs    |  2 +-
 tests/unit_node/_fs/_fs_handle_test.ts        | 39 ++++++++++
 tools/core_import_map.json                    |  2 +-
 8 files changed, 118 insertions(+), 80 deletions(-)
 delete mode 100644 ext/node/polyfills/_fs/_fs_writev.d.ts
 rename ext/node/polyfills/_fs/{_fs_writev.mjs => _fs_writev.ts} (52%)

diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index 64b6c006a1..731cd3a9c7 100644
--- a/ext/node/lib.rs
+++ b/ext/node/lib.rs
@@ -487,7 +487,7 @@ deno_core::extension!(deno_node,
     "_fs/_fs_watch.ts",
     "_fs/_fs_write.mjs",
     "_fs/_fs_writeFile.ts",
-    "_fs/_fs_writev.mjs",
+    "_fs/_fs_writev.ts",
     "_next_tick.ts",
     "_process/exiting.ts",
     "_process/process.ts",
diff --git a/ext/node/polyfills/_fs/_fs_writev.d.ts b/ext/node/polyfills/_fs/_fs_writev.d.ts
deleted file mode 100644
index 1ba5511ff1..0000000000
--- a/ext/node/polyfills/_fs/_fs_writev.d.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2018-2025 the Deno authors. MIT license.
-// Forked from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/d9df51e34526f48bef4e2546a006157b391ad96c/types/node/fs.d.ts
-
-import { ErrnoException } from "ext:deno_node/_global.d.ts";
-
-/**
- * Write an array of `ArrayBufferView`s to the file specified by `fd` using`writev()`.
- *
- * `position` is the offset from the beginning of the file where this data
- * should be written. If `typeof position !== 'number'`, the data will be written
- * at the current position.
- *
- * The callback will be given three arguments: `err`, `bytesWritten`, and`buffers`. `bytesWritten` is how many bytes were written from `buffers`.
- *
- * If this method is `util.promisify()` ed, it returns a promise for an`Object` with `bytesWritten` and `buffers` properties.
- *
- * It is unsafe to use `fs.writev()` multiple times on the same file without
- * waiting for the callback. For this scenario, use {@link createWriteStream}.
- *
- * On Linux, positional writes don't work when the file is opened in append mode.
- * The kernel ignores the position argument and always appends the data to
- * the end of the file.
- * @since v12.9.0
- */
-export function writev(
-  fd: number,
-  buffers: ReadonlyArray<ArrayBufferView>,
-  cb: (
-    err: ErrnoException | null,
-    bytesWritten: number,
-    buffers: ArrayBufferView[],
-  ) => void,
-): void;
-export function writev(
-  fd: number,
-  buffers: ReadonlyArray<ArrayBufferView>,
-  position: number | null,
-  cb: (
-    err: ErrnoException | null,
-    bytesWritten: number,
-    buffers: ArrayBufferView[],
-  ) => void,
-): void;
-export interface WriteVResult {
-  bytesWritten: number;
-  buffers: ArrayBufferView[];
-}
-export namespace writev {
-  function __promisify__(
-    fd: number,
-    buffers: ReadonlyArray<ArrayBufferView>,
-    position?: number,
-  ): Promise<WriteVResult>;
-}
-/**
- * For detailed information, see the documentation of the asynchronous version of
- * this API: {@link writev}.
- * @since v12.9.0
- * @return The number of bytes written.
- */
-export function writevSync(
-  fd: number,
-  buffers: ReadonlyArray<ArrayBufferView>,
-  position?: number,
-): number;
diff --git a/ext/node/polyfills/_fs/_fs_writev.mjs b/ext/node/polyfills/_fs/_fs_writev.ts
similarity index 52%
rename from ext/node/polyfills/_fs/_fs_writev.mjs
rename to ext/node/polyfills/_fs/_fs_writev.ts
index 61807c7f68..e38b44b19a 100644
--- a/ext/node/polyfills/_fs/_fs_writev.mjs
+++ b/ext/node/polyfills/_fs/_fs_writev.ts
@@ -5,15 +5,53 @@
 // deno-lint-ignore-file prefer-primordials
 
 import { Buffer } from "node:buffer";
+import process from "node:process";
+import { ErrnoException } from "ext:deno_node/_global.d.ts";
 import { validateBufferArray } from "ext:deno_node/internal/fs/utils.mjs";
 import { getValidatedFd } from "ext:deno_node/internal/fs/utils.mjs";
+import { WriteVResult } from "ext:deno_node/internal/fs/handle.ts";
 import { maybeCallback } from "ext:deno_node/_fs/_fs_common.ts";
 import * as io from "ext:deno_io/12_io.js";
 import { op_fs_seek_async, op_fs_seek_sync } from "ext:core/ops";
 
-export function writev(fd, buffers, position, callback) {
+export interface WriteVResult {
+  bytesWritten: number;
+  buffers: ReadonlyArray<ArrayBufferView>;
+}
+
+type writeVCallback = (
+  err: ErrnoException | null,
+  bytesWritten: number,
+  buffers: ReadonlyArray<ArrayBufferView>,
+) => void;
+
+/**
+ * Write an array of `ArrayBufferView`s to the file specified by `fd` using`writev()`.
+ *
+ * `position` is the offset from the beginning of the file where this data
+ * should be written. If `typeof position !== 'number'`, the data will be written
+ * at the current position.
+ *
+ * The callback will be given three arguments: `err`, `bytesWritten`, and`buffers`. `bytesWritten` is how many bytes were written from `buffers`.
+ *
+ * If this method is `util.promisify()` ed, it returns a promise for an`Object` with `bytesWritten` and `buffers` properties.
+ *
+ * It is unsafe to use `fs.writev()` multiple times on the same file without
+ * waiting for the callback. For this scenario, use {@link createWriteStream}.
+ *
+ * On Linux, positional writes don't work when the file is opened in append mode.
+ * The kernel ignores the position argument and always appends the data to
+ * the end of the file.
+ * @since v12.9.0
+ */
+export function writev(
+  fd: number,
+  buffers: ReadonlyArray<ArrayBufferView>,
+  position?: number | null,
+  callback?: writeVCallback,
+): void {
   const innerWritev = async (fd, buffers, position) => {
-    const chunks = [];
+    const chunks: Buffer[] = [];
     const offset = 0;
     for (let i = 0; i < buffers.length; i++) {
       if (Buffer.isBuffer(buffers[i])) {
@@ -45,16 +83,24 @@ export function writev(fd, buffers, position, callback) {
   if (typeof position !== "number") position = null;
 
   innerWritev(fd, buffers, position).then(
-    (nwritten) => {
-      callback(null, nwritten, buffers);
-    },
+    (nwritten) => callback(null, nwritten, buffers),
     (err) => callback(err),
   );
 }
 
-export function writevSync(fd, buffers, position) {
+/**
+ * For detailed information, see the documentation of the asynchronous version of
+ * this API: {@link writev}.
+ * @since v12.9.0
+ * @return The number of bytes written.
+ */
+export function writevSync(
+  fd: number,
+  buffers: ArrayBufferView[],
+  position?: number | null,
+): number {
   const innerWritev = (fd, buffers, position) => {
-    const chunks = [];
+    const chunks: Buffer[] = [];
     const offset = 0;
     for (let i = 0; i < buffers.length; i++) {
       if (Buffer.isBuffer(buffers[i])) {
@@ -85,3 +131,16 @@ export function writevSync(fd, buffers, position) {
 
   return innerWritev(fd, buffers, position);
 }
+
+export function writevPromise(
+  fd: number,
+  buffers: ArrayBufferView[],
+  position?: number,
+): Promise<WriteVResult> {
+  return new Promise((resolve, reject) => {
+    writev(fd, buffers, position, (err, bytesWritten, buffers) => {
+      if (err) reject(err);
+      else resolve({ bytesWritten, buffers });
+    });
+  });
+}
diff --git a/ext/node/polyfills/fs.ts b/ext/node/polyfills/fs.ts
index 76ff9ebd12..a6e43b5baa 100644
--- a/ext/node/polyfills/fs.ts
+++ b/ext/node/polyfills/fs.ts
@@ -119,7 +119,7 @@ import {
 // @deno-types="./_fs/_fs_write.d.ts"
 import { write, writeSync } from "ext:deno_node/_fs/_fs_write.mjs";
 // @deno-types="./_fs/_fs_writev.d.ts"
-import { writev, writevSync } from "ext:deno_node/_fs/_fs_writev.mjs";
+import { writev, writevSync } from "ext:deno_node/_fs/_fs_writev.ts";
 import { readv, readvSync } from "ext:deno_node/_fs/_fs_readv.ts";
 import {
   writeFile,
diff --git a/ext/node/polyfills/internal/fs/handle.ts b/ext/node/polyfills/internal/fs/handle.ts
index 2d787a9f3b..4244d159b7 100644
--- a/ext/node/polyfills/internal/fs/handle.ts
+++ b/ext/node/polyfills/internal/fs/handle.ts
@@ -6,7 +6,7 @@
 import { EventEmitter } from "node:events";
 import { Buffer } from "node:buffer";
 import { Mode, promises, read, write } from "node:fs";
-export type { BigIntStats, Stats } from "ext:deno_node/_fs/_fs_stat.ts";
+import { core } from "ext:core/mod.js";
 import {
   BinaryOptionsArgument,
   FileOptionsArgument,
@@ -14,7 +14,8 @@ import {
   TextOptionsArgument,
 } from "ext:deno_node/_fs/_fs_common.ts";
 import { ftruncatePromise } from "ext:deno_node/_fs/_fs_ftruncate.ts";
-import { core } from "ext:core/mod.js";
+export type { BigIntStats, Stats } from "ext:deno_node/_fs/_fs_stat.ts";
+import { writevPromise, WriteVResult } from "ext:deno_node/_fs/_fs_writev.ts";
 
 interface WriteResult {
   bytesWritten: number;
@@ -64,7 +65,7 @@ export class FileHandle extends EventEmitter {
           position,
           (err, bytesRead, buffer) => {
             if (err) reject(err);
-            else resolve({ buffer: buffer, bytesRead: bytesRead });
+            else resolve({ buffer, bytesRead });
           },
         );
       });
@@ -72,7 +73,7 @@ export class FileHandle extends EventEmitter {
       return new Promise((resolve, reject) => {
         read(this.fd, bufferOrOpt, (err, bytesRead, buffer) => {
           if (err) reject(err);
-          else resolve({ buffer: buffer, bytesRead: bytesRead });
+          else resolve({ buffer, bytesRead });
         });
       });
     }
@@ -137,6 +138,10 @@ export class FileHandle extends EventEmitter {
     return fsCall(promises.writeFile, this, data, options);
   }
 
+  writev(buffers: ArrayBufferView[], position?: number): Promise<WriteVResult> {
+    return fsCall(writevPromise, this, buffers, position);
+  }
+
   close(): Promise<void> {
     // Note that Deno.close is not async
     return Promise.resolve(core.close(this.fd));
diff --git a/ext/node/polyfills/internal/fs/streams.mjs b/ext/node/polyfills/internal/fs/streams.mjs
index 5adb8da225..d2ebecbe9a 100644
--- a/ext/node/polyfills/internal/fs/streams.mjs
+++ b/ext/node/polyfills/internal/fs/streams.mjs
@@ -18,7 +18,7 @@ import { errorOrDestroy } from "ext:deno_node/internal/streams/destroy.mjs";
 import { open as fsOpen } from "ext:deno_node/_fs/_fs_open.ts";
 import { read as fsRead } from "ext:deno_node/_fs/_fs_read.ts";
 import { write as fsWrite } from "ext:deno_node/_fs/_fs_write.mjs";
-import { writev as fsWritev } from "ext:deno_node/_fs/_fs_writev.mjs";
+import { writev as fsWritev } from "ext:deno_node/_fs/_fs_writev.ts";
 import { close as fsClose } from "ext:deno_node/_fs/_fs_close.ts";
 import { Buffer } from "node:buffer";
 import {
diff --git a/tests/unit_node/_fs/_fs_handle_test.ts b/tests/unit_node/_fs/_fs_handle_test.ts
index e4a41f8ba7..a8ca826329 100644
--- a/tests/unit_node/_fs/_fs_handle_test.ts
+++ b/tests/unit_node/_fs/_fs_handle_test.ts
@@ -118,6 +118,45 @@ Deno.test("[node/fs filehandle.writeFile] Write to file", async function () {
   assertEquals(decoder.decode(data), "hello world");
 });
 
+Deno.test(
+  "[node/fs filehandle.writev] Write array of buffers to file",
+  async function () {
+    const tempFile: string = await Deno.makeTempFile();
+    const fileHandle = await fs.open(tempFile, "w");
+
+    const buffer1 = Buffer.from("hello ");
+    const buffer2 = Buffer.from("world");
+    const res = await fileHandle.writev([buffer1, buffer2]);
+
+    const data = Deno.readFileSync(tempFile);
+    await Deno.remove(tempFile);
+    await fileHandle.close();
+
+    assertEquals(res.bytesWritten, 11);
+    assertEquals(decoder.decode(data), "hello world");
+  },
+);
+
+Deno.test(
+  "[node/fs filehandle.writev] Write array of buffers to file with position",
+  async function () {
+    const tempFile: string = await Deno.makeTempFile();
+    const fileHandle = await fs.open(tempFile, "w");
+
+    const buffer1 = Buffer.from("hello ");
+    const buffer2 = Buffer.from("world");
+    await fileHandle.writev([buffer1, buffer2], 0);
+    const buffer3 = Buffer.from("lorem ipsum");
+    await fileHandle.writev([buffer3], 6);
+
+    const data = Deno.readFileSync(tempFile);
+    await Deno.remove(tempFile);
+    await fileHandle.close();
+
+    assertEquals(decoder.decode(data), "hello lorem ipsum");
+  },
+);
+
 Deno.test(
   "[node/fs filehandle.truncate] Truncate file with length",
   async function () {
diff --git a/tools/core_import_map.json b/tools/core_import_map.json
index d38221eb4c..feeae1ac70 100644
--- a/tools/core_import_map.json
+++ b/tools/core_import_map.json
@@ -38,7 +38,7 @@
     "ext:deno_node/_fs/_fs_stat.ts": "../ext/node/polyfills/_fs/_fs_stat.ts",
     "ext:deno_node/_fs/_fs_watch.ts": "../ext/node/polyfills/_fs/_fs_watch.ts",
     "ext:deno_node/_fs/_fs_write.mjs": "../ext/node/polyfills/_fs/_fs_write.mjs",
-    "ext:deno_node/_fs/_fs_writev.mjs": "../ext/node/polyfills/_fs/_fs_writev.mjs",
+    "ext:deno_node/_fs/_fs_writev.ts": "../ext/node/polyfills/_fs/_fs_writev.ts",
     "ext:deno_node/_global.d.ts": "../ext/node/polyfills/_global.d.ts",
     "node:_http_agent": "../ext/node/polyfills/_http_agent.mjs",
     "node:_http_common": "../ext/node/polyfills/_http_common.ts",