first commit
This commit is contained in:
114
docs/devtools/tooling/readline/src/reader/reader.md
Normal file
114
docs/devtools/tooling/readline/src/reader/reader.md
Normal 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.
|
||||
Reference in New Issue
Block a user