first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,114 @@
# reader
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.