]> git.ardis.lu - csvio/commitdiff
Add public status property to CSVWriter for tracking progress
authorArdis Lu <ardisglu@gmail.com>
Mon, 27 Jan 2025 06:43:02 +0000 (22:43 -0800)
committerArdis Lu <ardisglu@gmail.com>
Mon, 27 Jan 2025 06:43:02 +0000 (22:43 -0800)
src/core.js
test/core.test.js

index 88093274ed817a72bf4c1415b5751c284253ee8b..00c953b59b56c6c95e6c7bfccdf58be61b5d5853 100644 (file)
@@ -1,6 +1,6 @@
 import { createReadStream } from 'node:fs';
 import { mkdir, open } from 'node:fs/promises';
-import { dirname, normalize } from 'node:path';
+import { dirname, basename, normalize } from 'node:path';
 import { Readable } from 'node:stream';
 import { fileURLToPath } from 'node:url';
 /** @import { PathLike } from 'node:fs'; */
@@ -327,6 +327,8 @@ export class CSVTransformer extends TransformStream {
  * @extends WritableStream
  */
 export class CSVWriter extends WritableStream {
+  #status;
+
   /**
    * @param {PathLike} path A `string`, `Buffer`, or `URL` representing a path to a local CSV file destination. If the file already
    * exists, its **data will be overwritten**.
@@ -334,6 +336,13 @@ export class CSVWriter extends WritableStream {
   constructor(path) {
     const fullPath = parsePathLike(path);
     const dir = dirname(fullPath);
+    const status = {
+      name: basename(fullPath),
+      start: performance.now(),
+      elapsed: 0,
+      rows: 0,
+      done: false
+    };
     let handle;
     super({
       async start() {
@@ -343,17 +352,47 @@ export class CSVWriter extends WritableStream {
       async write(chunk) {
         let data;
         try {
-          data = CSVWriter.arrayToCSVString(JSON.parse(chunk));
+          const parsed = JSON.parse(chunk);
+          data = CSVWriter.arrayToCSVString(parsed);
+          status.rows += parsed.length;
         }
         catch {
           data = chunk;
+          status.rows++;
         }
         await handle.write(data);
       },
       async close() {
         await handle.close();
+        status.elapsed = performance.now() - status.start;
+        status.done = true;
       }
     });
+
+    this.#status = status;
+  }
+
+  /**
+   * @typedef CSVWriterStatus The current status of the CSVWriter.
+   * @property {string} name The name of the output CSV file that this writer is writing to.
+   * @property {number} elapsed The number of milliseconds that have elapsed between this writer's creation and the output CSV file
+   * handle closing, or until now if the writer is still writing. Measured using `performance.now()`.
+   * @property {number} rows The estimated number of CSV rows that have been written. If the data passed to the writer is
+   * a string, the string is counted as one CSV row (i.e., raw string data is NOT parsed again).
+   * @property {boolean} done If `true`, the writer is finished writing to the output CSV file and the file handle is closed.
+   */
+
+  /** @type {CSVWriterStatus} */
+  get status() {
+    if (!this.#status.done) {
+      this.#status.elapsed = performance.now() - this.#status.start;
+    }
+    return {
+      name: this.#status.name,
+      elapsed: this.#status.elapsed,
+      rows: this.#status.rows,
+      done: this.#status.done
+    }
   }
 
   /**
index b0645098cd192e2067415ec323b6304ac551ce73..9418c38239d816402e10074a5de7def3e78027d1 100644 (file)
@@ -1,7 +1,8 @@
 import { suite, test } from 'node:test';
-import { deepStrictEqual, throws, rejects } from 'node:assert/strict';
+import { ok, deepStrictEqual, throws, rejects } from 'node:assert/strict';
 import { unlink } from 'node:fs/promises';
-import { normalize, resolve } from 'node:path';
+import { normalize, basename, resolve } from 'node:path';
+import { setTimeout } from 'node:timers/promises';
 import { pathToFileURL } from 'node:url';
 
 import { csvStreamEqualWritable, csvStreamNotEqualWritable, createCSVMockStream, createTempFile } from './utils.js';
@@ -478,4 +479,45 @@ suite('CSVWriter', { concurrency: true }, () => {
       await new CSVReader(temp).pipeTo(csvStreamEqualWritable(csv));
     });
   }
+});
+
+suite('CSVWriter status', { concurrency: true }, () => {
+  test('records file name', { concurrency: true }, async (t) => {
+    const temp = await createTempFile();
+    t.after(async () => await unlink(temp));
+    const writer = new CSVWriter(temp);
+    await createCSVMockStream([['']]).pipeTo(writer);
+    deepStrictEqual(writer.status.name, basename(temp));
+  });
+  test('records elapsed time', { concurrency: true }, async (t) => {
+    const temp = await createTempFile();
+    t.after(async () => await unlink(temp));
+    const writer = new CSVWriter(temp);
+    await createCSVMockStream([['']]).pipeTo(writer);
+    ok(writer.status.elapsed > 0);
+  });
+  test('elapsed time does not change after finishing writing', { concurrency: true }, async (t) => {
+    const temp = await createTempFile();
+    t.after(async () => await unlink(temp));
+    const writer = new CSVWriter(temp);
+    await createCSVMockStream([['']]).pipeTo(writer);
+    const { elapsed: elapsed1 } = writer.status;
+    await setTimeout(5);
+    const { elapsed: elapsed2 } = writer.status;
+    deepStrictEqual(elapsed1, elapsed2);
+  });
+  test('records rows written', { concurrency: true }, async (t) => {
+    const temp = await createTempFile();
+    t.after(async () => await unlink(temp));
+    const writer = new CSVWriter(temp);
+    await createCSVMockStream([[''], [''], ['']]).pipeTo(writer);
+    deepStrictEqual(writer.status.rows, 3);
+  });
+  test('records done status', { concurrency: true }, async (t) => {
+    const temp = await createTempFile();
+    t.after(async () => await unlink(temp));
+    const writer = new CSVWriter(temp);
+    await createCSVMockStream([['']]).pipeTo(writer);
+    ok(writer.status.done);
+  });
 });
\ No newline at end of file
This page took 0.047465 seconds and 4 git commands to generate.