[Example 4: Using `createCSVNormalizationStream` and `createCSVDenormalizationStream`](./ex4.js)
-[Example 5: Handling an irregularly-shaped CSV file](./ex5.js)
\ No newline at end of file
+[Example 5: Handling an irregularly-shaped CSV file](./ex5.js)
+
+[Example 6: Handling a flaky transformation (basic retry strategy)](./ex6.js)
\ No newline at end of file
--- /dev/null
+columnA\r
+1\r
+2\r
+3\r
+4\r
+5\r
--- /dev/null
+columnA\r
+1: pass\r
+2: pass\r
+3: pass\r
+4: pass\r
+5: pass\r
--- /dev/null
+// ## Example 6: Handling a flaky transformation (basic retry strategy)
+//
+// Pass a function to the `onError` option of `createCSVTransformStream` to catch errors thrown by the transformation
+// function.
+//
+// The `onError` function may be used to implement graceful failure strategies (e.g., set the output row to a
+// placeholder), retry strategies (e.g., a truncated exponential backoff for flaky network requests), logging, etc.
+
+import { createCSVReadableStream, createCSVTransformStream, createCSVWritableStream } from '../src/index.js';
+
+// Will immediately retry calling `fn` a maximum of `iterations` times
+function retry(row, fn, iterations) {
+ while (iterations--) {
+ try { return fn(row); }
+ catch { }
+ }
+}
+
+// Will fail 90% of the time
+function flaky(row) {
+ if (Math.random() < 0.9) {
+ throw new Error();
+ }
+ const [v] = row;
+ return [`${v}: pass`];
+}
+
+await createCSVReadableStream(new URL('./data/ex6-in.csv', import.meta.url))
+ .pipeThrough(createCSVTransformStream(flaky, { onError: (row, e, fn) => retry(row, fn, 1000) }))
+ .pipeTo(createCSVWritableStream(new URL('./data/ex6-out.csv', import.meta.url)));
\ No newline at end of file
* using `JSON.parse()` before being sent to `fn()`. The default value is `false`.
* @property {boolean} [rawOutput=false] Set to `true` to send the raw return value of `fn()` to the next stream. Otherwise, the return value of
* `fn()` will be serialized using `JSON.stringify()` before being sent to the next stream. The default value is `false`.
+ * @property {null|function(Array<string>|string,Error,function(Array<string>|string):Array<Array<any>>|Array<any>|string|null):Array<Array<any>>|Array<any>|string|null} [onError=null]
+ * Set to a function to catch errors thrown by the transformation function. The CSV row, the error that was thrown, and the transformation function
+ * itself will be passed to the `onError` function. The default value is `null` (errors will not be caught).
*/
/**
options.includeHeaders ??= false;
options.rawInput ??= false;
options.rawOutput ??= false;
+ options.onError ??= null;
+
+ async function wrappedFn(row) {
+ if (options.onError === null) { return await fn(row); }
+ try { return await fn(row); }
+ catch (e) { return await options.onError(row, e, fn); }
+ }
let firstChunk = true;
return new TransformStream({
let out;
if (firstChunk) {
firstChunk = false;
- out = options.includeHeaders ? await fn(row) : row;
+ out = options.includeHeaders ? await wrappedFn(row) : row;
}
else {
- out = await fn(row);
+ out = await wrappedFn(row);
}
if (out === null || out === undefined) { // Input row is consumed without emitting any output row
import { suite, test } from 'node:test';
-import { deepStrictEqual, throws } from 'node:assert/strict';
+import { deepStrictEqual, throws, rejects } from 'node:assert/strict';
import { unlink } from 'node:fs/promises';
import { normalize, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
['a', 'b']
]));
});
+ test('passes errors to onError function', { concurrency: true }, async () => {
+ await createCSVMockStream([
+ ['columnA'],
+ ['1'],
+ ['2']
+ ])
+ .pipeThrough(createCSVTransformStream(() => { throw new Error(); }, { onError: r => [`${r[0]}: caught`] }))
+ .pipeTo(csvStreamEqualWritable([
+ ['columnA'],
+ ['1: caught'],
+ ['2: caught']
+ ]));
+ });
+ test('bubbles up errors re-raised by onError function', { concurrency: true }, async () => {
+ await rejects(
+ createCSVMockStream([
+ ['columnA'],
+ ['1']
+ ])
+ .pipeThrough(createCSVTransformStream(() => { throw new Error('outer'); }, { onError: () => { throw new Error('inner') } }))
+ .pipeTo(csvStreamEqualWritable([
+ ['columnA'],
+ ['1']
+ ])),
+ { name: 'Error', message: 'inner' }
+ );
+ });
});
suite('createCSVWritableStream', { concurrency: true }, () => {