From: Ardis Lu Date: Wed, 15 Jan 2025 08:28:40 +0000 (-0800) Subject: Add onError callback option X-Git-Tag: v0.1.0~122 X-Git-Url: https://git.ardis.lu/?a=commitdiff_plain;h=e9472f58f6da30cdcc79f1fe0dc1d15c75baeb1f;p=csvio Add onError callback option --- diff --git a/examples/README.md b/examples/README.md index 808c82a..ec07240 100644 --- a/examples/README.md +++ b/examples/README.md @@ -18,4 +18,6 @@ node ./examples/ex1.js [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 diff --git a/examples/data/ex6-in.csv b/examples/data/ex6-in.csv new file mode 100644 index 0000000..4dad4af --- /dev/null +++ b/examples/data/ex6-in.csv @@ -0,0 +1,6 @@ +columnA +1 +2 +3 +4 +5 diff --git a/examples/data/ex6-out.csv b/examples/data/ex6-out.csv new file mode 100644 index 0000000..9bcd49e --- /dev/null +++ b/examples/data/ex6-out.csv @@ -0,0 +1,6 @@ +columnA +1: pass +2: pass +3: pass +4: pass +5: pass diff --git a/examples/ex6.js b/examples/ex6.js new file mode 100644 index 0000000..039de8c --- /dev/null +++ b/examples/ex6.js @@ -0,0 +1,30 @@ +// ## 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 diff --git a/src/core.js b/src/core.js index 6cd25b5..63e9888 100644 --- a/src/core.js +++ b/src/core.js @@ -138,6 +138,9 @@ export function createCSVReadableStream(path) { * 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,Error,function(Array|string):Array>|Array|string|null):Array>|Array|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). */ /** @@ -160,6 +163,13 @@ export function createCSVTransformStream(fn, options = {}) { 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({ @@ -168,10 +178,10 @@ export function createCSVTransformStream(fn, options = {}) { 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 diff --git a/test/core.test.js b/test/core.test.js index 98256e5..35ae2dd 100644 --- a/test/core.test.js +++ b/test/core.test.js @@ -1,5 +1,5 @@ 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'; @@ -245,6 +245,33 @@ suite('createCSVTransformStream', { concurrency: true }, () => { ['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 }, () => {