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
readLinesopens a read stream automatically viacreateReadStream - Node.js
Readableinput — pass anyReadable(stdin,net.Socket, custom, etc.) directly - WHATWG
ReadableStreaminput — pass a Web StreamsReadableStream(e.g. fromfetch().body); bridged to NodeReadableviaReadable.fromWeb skipEmptyoption — filter out blank lines before they reach the callerAbortSignalsupport — stop iteration at any point by aborting the controller; the generator exits cleanly on the next yield boundarymaxLineLengthandencodingpassthrough — forwarded verbatim tocreateScanner
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.