# reader _(no submodules)_ ## Purpose The `reader` module exposes `readLines()`, a unified async-generator entry point for line-by-line reading from any of three input forms: a Node.js `Readable` stream, a WHATWG `ReadableStream`, or a plain file-path string. It bridges the gap between the low-level `createScanner` byte-parser and the diverse I/O surfaces consumers work with in real applications. By accepting all three input kinds under a single function signature, `readLines()` lets callers swap between reading from stdin, a web `fetch()` response body, or an on-disk log file without changing any surrounding code. The function delegates all byte-parsing and line-assembly work to `createScanner`, keeping its own concerns limited to source normalisation and the `skipEmpty` filter. ## Features - **File-path string input** — pass a filesystem path and `readLines` opens a read stream automatically via `createReadStream` - **Node.js `Readable` input** — pass any `Readable` (stdin, `net.Socket`, custom, etc.) directly - **WHATWG `ReadableStream` input** — pass a Web Streams `ReadableStream` (e.g. from `fetch().body`); bridged to Node `Readable` via `Readable.fromWeb` - **`skipEmpty` option** — filter out blank lines before they reach the caller - **`AbortSignal` support** — stop iteration at any point by aborting the controller; the generator exits cleanly on the next yield boundary - **`maxLineLength` and `encoding` passthrough** — forwarded verbatim to `createScanner` ## Usage: Basic Example Read every line from a local file: ```ts import { readLines } from "@suaveplan/readline/reader/reader"; for await (const line of readLines("/var/log/app.log")) { console.log(line); } ``` Read from stdin: ```ts import { readLines } from "@suaveplan/readline/reader/reader"; import { stdin } from "node:process"; for await (const line of readLines(stdin)) { process.stdout.write(line + "\n"); } ``` ## Usage: Advanced Example Fetch a remote newline-delimited JSON stream, skip blank lines, and abort after the first error event: ```ts import { readLines } from "@suaveplan/readline/reader/reader"; const controller = new AbortController(); const response = await fetch("https://example.com/events", { signal: controller.signal, }); if (!response.body) throw new Error("No body"); const results: unknown[] = []; try { for await (const line of readLines(response.body, { skipEmpty: true, signal: controller.signal, maxLineLength: 65536, })) { const event = JSON.parse(line) as { type: string; payload: unknown }; results.push(event.payload); if (event.type === "error") { controller.abort(); } } } catch { // generator exits cleanly; partially collected results are still usable } ``` ## API Reference ### `readLines(input, options?)` ```ts async function* readLines( input: Readable | ReadableStream | string, options?: ReadLinesOptions ): AsyncGenerator ``` **Parameters** | Parameter | Type | Description | |---|---|---| | `input` | `Readable \| ReadableStream \| string` | The data source. A string is treated as a file path. | | `options.maxLineLength` | `number` (optional) | Maximum bytes per line before a `LINE_TOO_LONG` error is thrown. Defaults to `4096`. | | `options.encoding` | `BufferEncoding` (optional) | Text encoding for decoded lines. Defaults to `"utf8"`. | | `options.skipEmpty` | `boolean` (optional) | When `true`, lines that are empty strings after decoding are silently dropped. | | `options.signal` | `AbortSignal` (optional) | Aborts iteration early. The generator exits on the next yield boundary without throwing. | **Returns** `AsyncGenerator` — each yielded value is one decoded line, CR/LF stripped. **Throws** `ReadlineError` with code `SCANNER.STREAM_ERROR` on underlying stream errors, or `SCANNER.LINE_TOO_LONG` when a line exceeds `maxLineLength`. ### `ReadLinesOptions` Interface re-exported from `reader.types.ts`. All fields are optional. ## Implementation Notes **WHATWG stream bridging.** `Readable.fromWeb` (available in Node ≥ 17 and Bun ≥ 1.0) converts a `ReadableStream` into a Node `Readable`. The import of `node:stream` is deferred (`await import`) so the static module graph does not load it for callers that only use WHATWG paths — though in practice all three code paths require `node:stream`, so this is mainly a style alignment with future environments where the module may be conditionally present. **Detection heuristic.** WHATWG streams are identified by the presence of a `.getReader` method, which is part of the `ReadableStream` spec and absent from Node `Readable`. This avoids an `instanceof` check that would fail across realm boundaries (e.g. cross-context `ReadableStream` objects from a `fetch` polyfill). **Delegation to `createScanner`.** All byte-parsing, CR/LF normalisation, buffer management, and `AbortSignal` checking live in `createScanner`. `readLines` is intentionally thin — it adds only source normalisation and the `skipEmpty` filter. This keeps each layer testable in isolation and avoids duplicating byte-level logic. **`skipEmpty` placement.** The empty-line filter is applied after `createScanner` yields, not inside the scanner, so that callers who need raw empty lines (e.g. protocol parsers that use blank lines as delimiters) can obtain them by omitting the option.