Files
skills/docs/devtools/tooling/readline/src/reader/reader.md
2026-05-29 08:20:56 +01:00

5.3 KiB

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:

import { readLines } from "@suaveplan/readline/reader/reader";

for await (const line of readLines("/var/log/app.log")) {
  console.log(line);
}

Read from stdin:

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:

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?)

async function* readLines(
  input: Readable | ReadableStream | string,
  options?: ReadLinesOptions
): AsyncGenerator<string, void, unknown>

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<string> — 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<Uint8Array> 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.