7.1 KiB
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<T, T> 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 = 0produces an empty stream immediately- Throws
RangeErrorformax < 0(both forms) - Configurable
highWaterMarkon the functional factory - Plugin-OO
LimitTransform.process(chunk)returns input unchanged within the limit, throwsStreamTransformFailedErroronce exceeded LimitTransform.toTransformStream()delegates tolimitStream(max)so abort + termination semantics are identical to the functional factory- Honors
options.signalbefore and after the count check dispose()resets the per-instanceprocesscounter so the contract is well-defined forStreamManager-managed lifecycles- Type-parameterised on
Tso callers get full inference
Usage: Basic Example
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]
import { LimitTransform } from "@suaveplan/streams/transforms/limit/limit";
const t = new LimitTransform<number>(2);
await t.process(1); // 1
await t.process(2); // 2
await t.process(3); // throws StreamTransformFailedError
Usage: Advanced Example
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]
import { LimitTransform } from "@suaveplan/streams/transforms/limit/limit";
const t = new LimitTransform<number>(3);
// Streaming form: emits at most 3 chunks then closes the readable side.
const source = new ReadableStream<number>({
start(c) {
for (let i = 0; i < 100; i++) c.enqueue(i);
c.close();
},
});
const out: number[] = [];
await source
.pipeThrough(t.toTransformStream<number, number>())
.pipeTo(
new WritableStream<number>({
write(v) {
out.push(v);
},
})
);
console.log(out); // [0, 1, 2]
API Reference
limitStream<T>(max: number, options?: TransformOptions): TransformStream<T, T>
Parameters:
max— maximum number of chunks to emit; must be>= 0.options.highWaterMark— WHATWG queue depth (default:1).
Returns: TransformStream<T, T>.
Throws (immediate): RangeError if max < 0.
class LimitTransform<T = unknown>
constructor(max: number)
max is the maximum number of chunks the transform will admit. Must be >= 0; otherwise the constructor throws RangeError.
process<U>(input: U, options?: TransformOptions): Promise<U>
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<A, B>(): TransformStream<A, B>
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.