Repository: denoland/deno
Pull request: feat(node): implement `FileHandle.readv()` method

Implements `filehandle.readv(buffers[, position])` method for the `FileHandle` class in `node:fs/promises`.
This is part of #25554 tracking `FileHandle` missing methods.
References: Node.js docs: https://nodejs.org/api/fs.html#filehandlereadvbuffers-position

**Changes**
  - Added `ReadVResult` interface and `readvPromise` function in `_fs_readv.ts`
  - Added `readv` method to `FileHandle` class in `handle.ts`
  - Added unit tests for `readv` with and without position parameter

<!--
Before submitting a PR, please read https://docs.deno.com/runtime/manual/references/contributing

1. Give the PR a descriptive title.

  Examples of good title:
    - fix(std/http): Fix race condition in server
    - docs(console): Update docstrings
    - feat(doc): Handle nested reexports

  Examples of bad title:
    - fix #7123
    - update docs
    - fix bugs

2. Ensure there is a related issue and it is referenced in the PR text.
3. Ensure there are tests that cover the changes.
4. Ensure `cargo test` passes.
5. Ensure `./tools/format.js` passes without changing files.
6. Ensure `./tools/lint.js` passes.
7. Open as a draft PR if your work is still in progress. The CI won't run
   all steps, but you can add '[ci]' to a commit message to force it to.
8. If you would like to run the benchmarks on the CI, add the 'ci-bench' label.
-->


Changed files:
filename: ext/node/polyfills/_fs/_fs_readv.ts
patch: @@ -18,6 +18,7 @@ import { customPromisifyArgs } from "ext:deno_node/internal/util.mjs";
 
 const {
   ObjectDefineProperty,
+  Promise,
   PromisePrototypeThen,
   TypedArrayPrototypeGetByteLength,
 } = primordials;
@@ -98,6 +99,11 @@ ObjectDefineProperty(readv, customPromisifyArgs, {
   enumerable: false,
 });
 
+export interface ReadVResult {
+  bytesRead: number;
+  buffers: readonly ArrayBufferView[];
+}
+
 export function readvSync(
   fd: number,
   buffers: readonly ArrayBufferView[],
@@ -137,3 +143,16 @@ export function readvSync(
 
   return readTotal;
 }
+
+export function readvPromise(
+  fd: number,
+  buffers: readonly ArrayBufferView[],
+  position?: number,
+): Promise<ReadVResult> {
+  return new Promise((resolve, reject) => {
+    readv(fd, buffers, position ?? null, (err, bytesRead, buffers) => {
+      if (err) reject(err);
+      else resolve({ bytesRead, buffers });
+    });
+  });
+}

filename: ext/node/polyfills/internal/fs/handle.ts
patch: @@ -26,6 +26,7 @@ import {
 } from "ext:deno_node/_fs/_fs_common.ts";
 import { ftruncatePromise } from "ext:deno_node/_fs/_fs_ftruncate.ts";
 import { writevPromise, WriteVResult } from "ext:deno_node/_fs/_fs_writev.ts";
+import { readvPromise, ReadVResult } from "ext:deno_node/_fs/_fs_readv.ts";
 import { fchmodPromise } from "ext:deno_node/_fs/_fs_fchmod.ts";
 import { fchownPromise } from "ext:deno_node/_fs/_fs_fchown.ts";
 import { fdatasyncPromise } from "ext:deno_node/_fs/_fs_fdatasync.ts";
@@ -172,6 +173,13 @@ export class FileHandle extends EventEmitter {
     return fsCall(writevPromise, "writev", this, buffers, position);
   }
 
+  readv(
+    buffers: readonly ArrayBufferView[],
+    position?: number,
+  ): Promise<ReadVResult> {
+    return fsCall(readvPromise, "readv", this, buffers, position);
+  }
+
   [kRef]() {
     this[kRefs]++;
   }

filename: tests/node_compat/config.jsonc
patch: @@ -560,6 +560,7 @@
     "parallel/test-fs-readfilesync-enoent.js": {},
     "parallel/test-fs-readlink-type-check.js": {},
     "parallel/test-fs-readSync-optional-params.js": {},
+    "parallel/test-fs-readv-promises.js": {},
     "parallel/test-fs-readv-promisify.js": {},
     "parallel/test-fs-ready-event-stream.js": {},
     "parallel/test-fs-realpath-buffer-encoding.js": {},

filename: tests/unit_node/_fs/_fs_handle_test.ts
patch: @@ -157,6 +157,38 @@ Deno.test(
   },
 );
 
+Deno.test(
+  "[node/fs filehandle.readv] Read into array of buffers from file",
+  async function () {
+    const fileHandle = await fs.open(testData);
+
+    const buffer1 = Buffer.alloc(5);
+    const buffer2 = Buffer.alloc(6);
+    const res = await fileHandle.readv([buffer1, buffer2]);
+
+    await fileHandle.close();
+
+    assertEquals(res.bytesRead, 11);
+    assertEquals(decoder.decode(buffer1), "hello");
+    assertEquals(decoder.decode(buffer2), " world");
+  },
+);
+
+Deno.test(
+  "[node/fs filehandle.readv] Read into array of buffers with position",
+  async function () {
+    const fileHandle = await fs.open(testData);
+
+    const buffer1 = Buffer.alloc(5);
+    const res = await fileHandle.readv([buffer1], 6);
+
+    await fileHandle.close();
+
+    assertEquals(res.bytesRead, 5);
+    assertEquals(decoder.decode(buffer1), "world");
+  },
+);
+
 Deno.test(
   "[node/fs filehandle.truncate] Truncate file with length",
   async function () {