]> git.ardis.lu - csvio/commitdiff
Add maxConcurrent option
authorArdis Lu <ardisglu@gmail.com>
Mon, 20 Jan 2025 07:11:23 +0000 (23:11 -0800)
committerArdis Lu <ardisglu@gmail.com>
Mon, 20 Jan 2025 07:11:23 +0000 (23:11 -0800)
examples/README.md
examples/data/ex8-in.csv [new file with mode: 0644]
examples/data/ex8-out.csv [new file with mode: 0644]
examples/ex8.js [new file with mode: 0644]
src/core.js

index e9fec20ac3deb2a4ddc387bf9bdd56af366b5242..3d97d78a20db90194283d5f6292b45744937285a 100644 (file)
@@ -22,4 +22,6 @@ node ./examples/ex1.js
 
 [Example 6: Handling a flaky transformation (basic retry strategy)](./ex6.js)
 
-[Example 7: Processing rows in batches (multi-row input)](./ex7.js)
\ No newline at end of file
+[Example 7: Processing rows in batches (multi-row input)](./ex7.js)
+
+[Example 8: Processing rows concurrently (async)](./ex8.js)
\ No newline at end of file
diff --git a/examples/data/ex8-in.csv b/examples/data/ex8-in.csv
new file mode 100644 (file)
index 0000000..99bd6d1
--- /dev/null
@@ -0,0 +1,9 @@
+concurrent group
+a
+a
+a
+a
+a
+b
+b
+b
diff --git a/examples/data/ex8-out.csv b/examples/data/ex8-out.csv
new file mode 100644 (file)
index 0000000..de20589
--- /dev/null
@@ -0,0 +1,9 @@
+tick number,concurrent group\r
+1,a\r
+1,a\r
+1,a\r
+1,a\r
+1,a\r
+2,b\r
+2,b\r
+2,b\r
diff --git a/examples/ex8.js b/examples/ex8.js
new file mode 100644 (file)
index 0000000..a44f6be
--- /dev/null
@@ -0,0 +1,32 @@
+// ## Example 8: Processing rows concurrently (async)
+// 
+// Set the `maxConcurrent` option to a number greater than `1` to process rows in parallel. The transformation function
+// will be automatically turned into a promise if it isn't already async.
+// 
+// Note that execution is blocked until all promises in a concurrent group settle. So if there is one transformation that
+// takes very long, all row processing is blocked on the 1 transformation even if all others in the group have already
+// resolved.
+
+import { setTimeout } from 'node:timers/promises';
+import { CSVReader, CSVTransformer, CSVWriter } from '../src/index.js';
+
+// Returns how many 100ms increments have elapsed since this program began
+const start = performance.now();
+function tick() {
+  const ms = performance.now() - start;
+  return Math.floor(ms / 100);
+}
+
+let firstChunk = true;
+async function parallel(row) {
+  if (firstChunk) {
+    firstChunk = false;
+    return ['tick number', ...row];
+  }
+  await setTimeout(100); // Simulate some slow task, e.g. a network request
+  return [tick(), ...row];
+}
+
+await new CSVReader(new URL('./data/ex8-in.csv', import.meta.url))
+  .pipeThrough(new CSVTransformer(parallel, { includeHeaders: true, maxConcurrent: 5 }))
+  .pipeTo(new CSVWriter(new URL('./data/ex8-out.csv', import.meta.url)));
\ No newline at end of file
index e3037c84328a013ea070fb99d5bda4cf76ae2761..b3a4c4051339feae1c19cba5ec3faaa309f94a39 100644 (file)
@@ -160,6 +160,10 @@ export class CSVReader extends ReadableStream {
  * to the `onError` function. The default value is `null` (errors will not be caught).
  * @property {Number} [maxBatchSize=1] The maximum number of rows that will be passed to the transformation function per function call (greedy).
  * The default value is `1`.
+ * @property {Number} [maxConcurrent=1] The maximum concurrent executions of the transformation function (greedy). The transformation function
+ * will be automatically turned into a promise if it isn't already async. Execution is blocked until all promises in a concurrent group settle
+ * (i.e., if one transformation in a group is hanging, further rows will NOT be processed even if all other transformations in the group are
+ * resolved). The default value is `1`.
  */
 
 /**
@@ -169,6 +173,7 @@ export class CSVReader extends ReadableStream {
 export class CSVTransformer extends TransformStream {
   #fn;
   #options;
+  #concurrent = [];
   #batch = [];
   #firstChunk = true;
 
@@ -201,6 +206,7 @@ export class CSVTransformer extends TransformStream {
     options.rawOutput ??= false;
     options.onError ??= null;
     options.maxBatchSize ??= 1;
+    options.maxConcurrent ??= 1;
 
     this.#fn = fn;
     this.#options = options;
@@ -228,32 +234,58 @@ export class CSVTransformer extends TransformStream {
     }
   }
 
+  async #enqueueConcurrent(controller) {
+    const results = await Promise.allSettled(this.#concurrent);
+    this.#concurrent.length = 0;
+    for (const r of results) {
+      const { status, value, reason } = r;
+      if (status === 'rejected') {
+        if (reason instanceof Error) {
+          throw reason;
+        }
+        else {
+          throw new Error(reason);
+        }
+      }
+      else {
+        this.#enqueueRow(value, controller);
+      }
+    }
+  }
+
   async #transform(chunk, controller) {
-    const { includeHeaders, rawInput, maxBatchSize } = this.#options;
+    const { includeHeaders, rawInput, maxBatchSize, maxConcurrent } = this.#options;
     const row = rawInput ? chunk : JSON.parse(chunk);
-    let out;
+
     if (this.#firstChunk) {
       this.#firstChunk = false;
-      out = includeHeaders ? await this.#wrappedFn(row) : row;
+      const out = includeHeaders ? await this.#wrappedFn(row) : row;
+      this.#enqueueRow(out, controller);
+      return;
     }
-    else if (maxBatchSize > 1) {
+
+    if (maxBatchSize > 1) {
       this.#batch.push(row);
       if (this.#batch.length === maxBatchSize) {
-        out = await this.#wrappedFn(this.#batch);
+        this.#concurrent.push(this.#wrappedFn(this.#batch));
         this.#batch.length = 0;
       }
     }
     else {
-      out = await this.#wrappedFn(row);
+      this.#concurrent.push(this.#wrappedFn(row));
     }
 
-    this.#enqueueRow(out, controller);
+    if (this.#concurrent.length === maxConcurrent) {
+      await this.#enqueueConcurrent(controller);
+    }
   }
 
   async #flush(controller) {
     if (this.#batch.length > 0) {
-      const out = await this.#wrappedFn(this.#batch);
-      this.#enqueueRow(out, controller);
+      this.#concurrent.push(this.#wrappedFn(this.#batch));
+    }
+    if (this.#concurrent.length > 0) {
+      await this.#enqueueConcurrent(controller);
     }
   }
 }
This page took 0.042085 seconds and 4 git commands to generate.