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'; */
* @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**.
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() {
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
+ }
}
/**
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';
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