# limit _(no submodules)_ ## Purpose The `limit` module exports both a functional factory (`limitStream`) and a plugin-OO wrapper (`LimitTransform`) that cap how many chunks may flow through a stream. `limitStream` creates a `TransformStream` that passes at most `max` chunks downstream and then terminates the stream by calling `controller.terminate()`. `LimitTransform` is the `BaseTransform` plugin form: in single-input mode (`process(chunk)`) it returns the input unchanged for the first `max` invocations and throws `StreamTransformFailedError` (`STREAM.TRANSFORM_FAILED`) on every subsequent call; in streaming mode (`toTransformStream()`) it delegates to `limitStream(max)`. This is the stream equivalent of `Array.prototype.slice(0, max)` and is useful when you only need the first N results from an infinite or very large stream — for example, paginated previews, search result limits, or safety caps on resource-intensive pipelines. Once the limit is reached, the writable side is closed and any further chunks from the source are not pulled, allowing backpressure to propagate naturally to the upstream producer. ## Features - Hard cap on output chunk count (`limitStream`) - Stream is terminated (not errored) once the limit is reached - `max = 0` produces an empty stream immediately - Throws `RangeError` for `max < 0` (both forms) - Configurable `highWaterMark` on the functional factory - Plugin-OO `LimitTransform.process(chunk)` returns input unchanged within the limit, throws `StreamTransformFailedError` once exceeded - `LimitTransform.toTransformStream()` delegates to `limitStream(max)` so abort + termination semantics are identical to the functional factory - Honors `options.signal` before and after the count check - `dispose()` resets the per-instance `process` counter so the contract is well-defined for `StreamManager`-managed lifecycles - Type-parameterised on `T` so callers get full inference ## Usage: Basic Example ```ts import { limitStream } from "@suaveplan/streams/transforms/limit/limit"; import { fromArray } from "@suaveplan/streams/readable/from-array/from-array"; import { toArray } from "@suaveplan/streams/sinks/to-array/to-array"; const result = await toArray( fromArray([1, 2, 3, 4, 5]).pipeThrough(limitStream(3)) ); console.log(result); // [1, 2, 3] ``` ```ts import { LimitTransform } from "@suaveplan/streams/transforms/limit/limit"; const t = new LimitTransform(2); await t.process(1); // 1 await t.process(2); // 2 await t.process(3); // throws StreamTransformFailedError ``` ## Usage: Advanced Example ```ts import { limitStream } from "@suaveplan/streams/transforms/limit/limit"; import { fromAsyncGenerator } from "@suaveplan/streams/readable/from-generator/from-generator"; import { toArray } from "@suaveplan/streams/sinks/to-array/to-array"; // Safely cap an infinite generator async function* infiniteCounter() { let n = 0; while (true) yield n++; } const first10 = await toArray( fromAsyncGenerator(infiniteCounter).pipeThrough(limitStream(10)) ); console.log(first10); // [0, 1, 2, ..., 9] ``` ```ts import { LimitTransform } from "@suaveplan/streams/transforms/limit/limit"; const t = new LimitTransform(3); // Streaming form: emits at most 3 chunks then closes the readable side. const source = new ReadableStream({ start(c) { for (let i = 0; i < 100; i++) c.enqueue(i); c.close(); }, }); const out: number[] = []; await source .pipeThrough(t.toTransformStream()) .pipeTo( new WritableStream({ write(v) { out.push(v); }, }) ); console.log(out); // [0, 1, 2] ``` ## API Reference ### `limitStream(max: number, options?: TransformOptions): TransformStream` **Parameters**: - `max` — maximum number of chunks to emit; must be `>= 0`. - `options.highWaterMark` — WHATWG queue depth (default: `1`). **Returns**: `TransformStream`. **Throws** (immediate): `RangeError` if `max < 0`. ### `class LimitTransform` #### `constructor(max: number)` `max` is the maximum number of chunks the transform will admit. Must be `>= 0`; otherwise the constructor throws `RangeError`. #### `process(input: U, options?: TransformOptions): Promise` Honors `options.signal` — rejects with `StreamAbortedError` (`STREAM.ABORTED`) when fired. Increments the per-instance counter and returns `input` unchanged while the counter is `<= max`. Once the counter has reached `max`, every further call rejects with `StreamTransformFailedError` (`STREAM.TRANSFORM_FAILED`). #### `toTransformStream(): TransformStream` Returns a fresh WHATWG `TransformStream` produced by `limitStream(max)`. The streaming form **does not throw** when the limit is reached — it silently terminates the readable side after enqueuing `max` chunks. This matches the historical `limitStream` contract. #### `dispose(): void` Resets the `process` counter to zero, clears all hooks/listeners, and calls `super.dispose()`. Idempotent. ### `interface LimitTransformConfig` `{ max: number }` — config record kept under `limit.types.ts` for `TransformFactory` integration. ## Implementation Notes `controller.terminate()` is called immediately after enqueuing the `max`-th chunk in `limitStream`. This closes the readable side of the transform stream while also signalling the writable side to stop accepting chunks. The WHATWG Streams specification defines `terminate()` as the correct way to close a transform from within the transformer — it is distinct from `close()` (which would only close the readable side and could leave the writable side open). `LimitTransform` is one of the rare stateful transforms in this package. Its state is a single `count` integer that tracks single-input `process` invocations. **Per-instance counter, not shared with the streaming form.** Each call to `toTransformStream()` constructs a fresh `TransformStream` via `limitStream(max)` which keeps its own internal counter inside the closure. That counter is independent from the class's `process` counter — running the streaming form to completion does not advance the `process` counter and vice versa. This is intentional: the two call paths represent different consumption models (one-at-a-time vs. pipe-through) and bleeding state between them would surprise callers. **Reset via `dispose()`.** The streaming form's counter is collected with the `TransformStream` itself when its readable side is consumed, so it needs no explicit reset. The `process` counter is per-instance and persists for the life of the `LimitTransform` instance. `dispose()` zeroes it before delegating to `super.dispose()` so a `StreamManager` reusing a transform instance after teardown gets a clean slate. **Single source of truth for streaming behaviour.** `toTransformStream()` delegates straight to `limitStream(max)` — no parallel implementation, no drift surface. Single-input semantics (throw on exceed) deliberately differ from streaming semantics (silently terminate) because the WHATWG `TransformStream` cannot signal "limit reached" without erroring the readable side, which is the wrong default for take-N composition.