first commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
# safeBranch
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`safeBranch` is a pipeline operator that wraps a sub-pipeline factory in a fault boundary. It runs the supplied `factory(input)` to construct an inner `ReadableStream`, then forwards every chunk it produces to the wrapper's output stream. If the inner pipeline throws — synchronously while the factory is being invoked, or asynchronously while its stream is being read — the wrapper's output stream closes cleanly (no further chunks emitted) and the error is forwarded to `options.onError` as a `StreamBranchFailedError` whose `cause` is the original error.
|
||||
|
||||
The outer pipeline never observes the inner error. This is the difference between a normal `pipeThrough` (which propagates errors and tears down the pipeline) and `safeBranch` (which contains them). Use it for opportunistic side branches — analytics, mirroring, telemetry — where a failure must not bring down the primary data path.
|
||||
|
||||
## Features
|
||||
|
||||
- Isolates errors from inner sub-pipelines so the outer pipeline keeps running
|
||||
- Forwards wrapped errors to `options.onError` as `StreamBranchFailedError`
|
||||
- Preserves the original error via the `cause` chain
|
||||
- Output stream closes cleanly after a branch error — no half-emitted state
|
||||
- Silent mode when `onError` is omitted
|
||||
- Synchronous factory throws are caught and reported the same way
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { fromArray } from "@suaveplan/streams/readable/from-array/from-array";
|
||||
import { safeBranch } from "@suaveplan/streams/pipeline/safe-branch/safe-branch";
|
||||
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";
|
||||
|
||||
const wrapped = safeBranch<number, number>(
|
||||
(input) =>
|
||||
input.pipeThrough(
|
||||
new TransformStream<number, number>({
|
||||
transform(chunk, c) {
|
||||
c.enqueue(chunk * 2);
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const out = wrapped(fromArray([1, 2, 3]));
|
||||
const result = await toArray(out); // [2, 4, 6]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { fromArray } from "@suaveplan/streams/readable/from-array/from-array";
|
||||
import { safeBranch } from "@suaveplan/streams/pipeline/safe-branch/safe-branch";
|
||||
import { StreamBranchFailedError } from "@suaveplan/streams/errors/stream-error";
|
||||
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";
|
||||
|
||||
let lastError: StreamBranchFailedError | undefined;
|
||||
|
||||
const analyticsBranch = safeBranch<number, number>(
|
||||
(input) =>
|
||||
input.pipeThrough(
|
||||
new TransformStream<number, number>({
|
||||
transform(chunk, c) {
|
||||
if (chunk < 0) throw new Error("negative not allowed");
|
||||
c.enqueue(chunk);
|
||||
},
|
||||
})
|
||||
),
|
||||
{
|
||||
onError: (err) => {
|
||||
lastError = err as StreamBranchFailedError;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const collected = await toArray(analyticsBranch(fromArray([1, -2, 3])));
|
||||
// collected captures whatever was emitted before the error;
|
||||
// the outer pipeline does not throw.
|
||||
console.log(lastError?.code); // "STREAM.BRANCH_FAILED"
|
||||
console.log(lastError?.cause); // Error: negative not allowed
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `safeBranch<TIn, TOut>(factory, options?)`
|
||||
|
||||
- `factory: (input: ReadableStream<TIn>) => ReadableStream<TOut>` — constructs the inner sub-pipeline. May throw synchronously; failures are isolated.
|
||||
- `options.onError?: (err: unknown) => void` — invoked with a `StreamBranchFailedError` when the inner pipeline errors. The original error is exposed via `err.cause`.
|
||||
- **Returns** `(input: ReadableStream<TIn>) => ReadableStream<TOut>` — a wrapper function suitable for use anywhere a stream-shape transform is accepted.
|
||||
- **Throws** Never. All errors are converted to `StreamBranchFailedError` and either delivered to `onError` or silently swallowed.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The wrapper acquires a single reader against the inner stream and pulls chunk-by-chunk inside the outer stream's `pull` callback. This preserves backpressure end-to-end: the outer consumer's pull cadence drives the inner sub-pipeline's read cadence.
|
||||
- On error, the wrapper sets a `closed` flag, closes the outer controller, and calls `onError`. Subsequent pulls return `done` without re-invoking the inner reader.
|
||||
- Cancellation of the wrapper propagates to the inner reader via `reader.cancel(reason)`. Cancellation errors from the inner stream are swallowed — they are part of the isolated branch by definition.
|
||||
- The error code is `STREAM.BRANCH_FAILED` (registered in `errors/stream-error.ts`). Inspect `err.code` to detect a branch failure programmatically; inspect `err.cause` to recover the original error.
|
||||
- `safeBranch` does not retry or restart the inner pipeline. After failure, the wrapper is terminal. To retry, pair `safeBranch` with `@suaveplan/retry` upstream of the factory.
|
||||
Reference in New Issue
Block a user