Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# tap
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The `tap` module exposes two complementary forms of the same side-effect probe: `tapStream`, the WHATWG `TransformStream` factory, and `TapTransform`, the plugin-OO wrapper used by `BaseTransform`-aware pipelines (`StreamManager`, `TransformFactory`, hook/event consumers). Both forms invoke a caller-supplied function for every chunk and forward the chunk downstream unchanged — the stream equivalent of a debug probe attached to logging, metrics, tracing, or test assertions.
Errors thrown by the side-effect are caught and re-wrapped as `StreamTransformFailedError`, erroring the stream and propagating upstream cancellation. For error-tolerant tapping (fire-and-forget) wrap the function in a try/catch before passing it in. The class form additionally honours `options.signal` before and after the side-effect via `assertNotAborted`, rejecting with `StreamAbortedError` when fired.
## Features
- Identity passthrough — chunks are never modified
- Sync and async side-effect functions
- Errors in `fn` error the stream (`StreamTransformFailedError`)
- AbortSignal support (`StreamAbortedError`)
- Configurable `highWaterMark`
- OO form (`TapTransform`) plugs into `BaseTransform`-driven pipelines
- Streaming form (`tapStream`) is the single source of truth — the OO form delegates to it
## Usage: Basic Example
```ts
import { tapStream } from "@suaveplan/streams/transforms/tap/tap";
import { fromArray } from "@suaveplan/streams/readable/from-array/from-array";
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";
const seen: number[] = [];
const result = await toArray(
fromArray([1, 2, 3]).pipeThrough(tapStream((n) => seen.push(n)))
);
console.log(result); // [1, 2, 3]
console.log(seen); // [1, 2, 3]
```
```ts
import { TapTransform } from "@suaveplan/streams/transforms/tap/tap";
const log = new TapTransform<string>((s) => console.log("[tap]", s));
await log.process("hello"); // logs "[tap] hello", returns "hello"
```
## Usage: Advanced Example
```ts
import { TapTransform, tapStream } from "@suaveplan/streams/transforms/tap/tap";
import { fromArray } from "@suaveplan/streams/readable/from-array/from-array";
import { mapStream } from "@suaveplan/streams/transforms/map/map";
import { toArray } from "@suaveplan/streams/sinks/to-array/to-array";
import { StreamManager } from "@suaveplan/streams/manager/manager/manager";
let chunkCount = 0;
const result = await toArray(
fromArray([10, 20, 30])
.pipeThrough(
tapStream((n) => {
chunkCount++;
console.log("before:", n);
})
)
.pipeThrough(mapStream((n) => n / 10))
.pipeThrough(tapStream((n) => console.log("after:", n)))
);
// logs: before: 10, after: 1, before: 20, after: 2, before: 30, after: 3
console.log(chunkCount); // 3
let total = 0;
const meter = new TapTransform<number>(async (n) => {
await Promise.resolve();
total += n;
});
await new StreamManager().pipe(meter).execute(5); // process(5) returns 5, total === 5
```
## API Reference
### `tapStream<T>(fn: (chunk: T) => void | Promise<void>, options?: TransformOptions): TransformStream<T, T>`
**Parameters**:
- `fn` — side-effect function; return value is ignored. May be async.
- `options.signal` — AbortSignal; errors the stream on abort.
- `options.highWaterMark` — WHATWG queue depth (default: `1`).
**Returns**: `TransformStream<T, T>`.
**Throws** (via error channel): `StreamTransformFailedError` when `fn` throws, `StreamAbortedError` when the signal fires.
### `class TapTransform<T = unknown>`
#### `constructor(fn: TapFn<T>)`
`fn` is `(chunk: T) => void | Promise<void>`. Any value returned by the function is ignored — only the original chunk is forwarded.
#### `process<U>(input: U, options?: TransformOptions): Promise<U>`
Awaits `fn(input)` and returns `input` unchanged. Honours `options.signal` — rejects with `StreamAbortedError` (`STREAM.ABORTED`) when fired before or after the side-effect runs.
#### `toTransformStream<A, B>(): TransformStream<A, B>`
Returns a fresh WHATWG `TransformStream` produced by `tapStream(fn)`. Errors thrown by `fn` are wrapped in `StreamTransformFailedError` (`STREAM.TRANSFORM_FAILED`) at the readable side.
### `type TapFn<T>`
`(chunk: T) => void | Promise<void>` — sync or async side-effect callback.
## Implementation Notes
`fn` is awaited before the chunk is enqueued, so an async tap delays throughput by the duration of the async operation. For fire-and-forget semantics (e.g. non-blocking analytics calls), pass a function that starts the async work but does not `await` it. The chunk passthrough (`controller.enqueue(chunk)`) executes only after `fn` resolves, ensuring ordering is preserved.
The OO class follows the canonical M2b `BaseTransform`-subclass pattern established by `MapTransform`:
1. The functional factory (`tapStream`) stays untouched — single source of truth for behaviour.
2. The OO subclass holds the configuration in a private readonly field and exposes `name` from the constructor.
3. `process(input, options)` runs the side-effect single-shot, calling `assertNotAborted` before and after the work, then returns the original input.
4. `toTransformStream()` returns the result of the functional factory directly — no parallel implementation, no drift surface.
Because tap is a stateless pass-through, there is no buffering, no batching, and no internal state to reset between invocations. Callers who need rate limiting on the side-effect should wrap `fn` themselves; this module deliberately stays unopinionated.