first commit
This commit is contained in:
116
docs/devtools/tooling/readline/src/interface/interface.md
Normal file
116
docs/devtools/tooling/readline/src/interface/interface.md
Normal file
@@ -0,0 +1,116 @@
|
||||
# interface
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`createInterface()` produces an interactive readline interface that wraps the lower-level `createScanner()` generator with a higher-level API suited for CLI tools, prompts, and line-by-line pipeline processing. It serves as the primary user-facing entry point for `@suaveplan/readline`, replacing `node:readline`'s callback-heavy `Interface` class with a fully promise-based, abort-signal-aware, and OpenTelemetry-instrumented alternative. Every interface instance is covered by a single OTel span that begins at construction and ends when `close()` is called or the async iterator drains naturally.
|
||||
|
||||
## Features
|
||||
|
||||
- **`question(prompt, signal?)`** — writes a prompt string to the optional output stream, then awaits the next line from input; rejects with `INTERFACE.CLOSED` when the interface is closed and `INTERFACE.ABORTED` when an `AbortSignal` fires
|
||||
- **`close()`** — idempotent close method; sets `closed = true` and ends the OTel span; safe to call multiple times
|
||||
- **`closed` getter** — synchronous boolean reflecting the current open/closed state
|
||||
- **Async iterator** (`for await...of`) — iterate all lines from the underlying scanner; automatically marks the interface closed when the stream drains
|
||||
- **AbortSignal support** — both the interface-level signal (passed to `createInterface`) and a per-question signal are honoured; abort mid-question rejects with `INTERFACE.ABORTED`
|
||||
- **OTel span lifetime** — a `readline.interface` span is started on construction and ended on close or stream drain; abort sets `SpanStatusCode.ERROR` before ending
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Readable } from "node:stream";
|
||||
import { createInterface } from "@suaveplan/readline";
|
||||
|
||||
const input = Readable.from("Alice\nBob\n");
|
||||
const rl = createInterface({ input });
|
||||
|
||||
const name = await rl.question("Enter name: ");
|
||||
console.log(`Hello, ${name}!`); // "Hello, Alice!"
|
||||
rl.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { Readable, Writable } from "node:stream";
|
||||
import { createInterface } from "@suaveplan/readline";
|
||||
|
||||
// Iterate all lines with abort support
|
||||
const controller = new AbortController();
|
||||
const input = Readable.from("line1\nline2\nline3\n");
|
||||
const rl = createInterface({ input, signal: controller.signal });
|
||||
|
||||
const collected: string[] = [];
|
||||
for await (const line of rl) {
|
||||
collected.push(line);
|
||||
if (line === "line2") controller.abort(); // stop early
|
||||
}
|
||||
// collected === ["line1", "line2"]
|
||||
|
||||
// Question with per-call signal override
|
||||
const perQuestion = new AbortController();
|
||||
const rl2 = createInterface({ input: Readable.from("response\n") });
|
||||
setTimeout(() => perQuestion.abort(), 100);
|
||||
try {
|
||||
const ans = await rl2.question("Say something: ", perQuestion.signal);
|
||||
console.log(ans);
|
||||
} catch (err) {
|
||||
// ReadlineError with code READLINE_CODES.INTERFACE.ABORTED
|
||||
}
|
||||
rl2.close();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `createInterface(options: ReadlineOptions): ReadlineInterface`
|
||||
|
||||
Creates and returns a new readline interface wrapping the provided `Readable` stream.
|
||||
|
||||
**Parameters**
|
||||
|
||||
| Name | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `options.input` | `Readable` | Yes | The source readable stream to scan line-by-line |
|
||||
| `options.output` | `Writable` | No | Optional writable stream; prompts are written here |
|
||||
| `options.signal` | `AbortSignal` | No | Optional interface-level abort signal |
|
||||
|
||||
**Returns** `ReadlineInterface`
|
||||
|
||||
---
|
||||
|
||||
### `ReadlineInterface`
|
||||
|
||||
| Member | Signature | Description |
|
||||
|--------|-----------|-------------|
|
||||
| `question` | `(prompt: string, signal?: AbortSignal) => Promise<string>` | Write prompt and await the next line. Throws `INTERFACE.CLOSED` if the interface is closed; throws `INTERFACE.ABORTED` if a signal fires |
|
||||
| `close` | `() => void` | Idempotent; marks the interface closed and ends the OTel span |
|
||||
| `closed` | `readonly boolean` | `true` after `close()` has been called |
|
||||
| `[Symbol.asyncIterator]` | `() => AsyncIterator<string>` | Iterate all remaining lines; stops when closed or stream drains |
|
||||
|
||||
---
|
||||
|
||||
### `ReadlineOptions`
|
||||
|
||||
```ts
|
||||
interface ReadlineOptions {
|
||||
input: Readable;
|
||||
output?: Writable;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
```
|
||||
|
||||
**Throws** `ReadlineError` with codes from `READLINE_CODES.INTERFACE`:
|
||||
|
||||
- `INTERFACE.CLOSED` — method called after `close()` or stream drained
|
||||
- `INTERFACE.ABORTED` — abort signal fired before or during a `question()` call
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
**OTel span design** — The span starts immediately in `createInterface()` (not lazily) so it faithfully represents the full interface lifetime. `span.end()` is called exactly once: either from `close()`, from the async iterator when the stream drains, or when an abort surfaces in the iterator. An abort path also calls `span.setStatus(ERROR)` before ending. Double-close is guarded by a `if (closed) return` early exit.
|
||||
|
||||
**Why `iter.next()` instead of `for await`** — `question()` needs to await exactly one line at a time, interleaved with prompt writes, which rules out `for await`. Calling `iter.next()` directly gives precise per-line control over the shared iterator and lets the abort handler race cleanly against the next-line promise without leaving dangling iterations.
|
||||
|
||||
**Double-abort guard** — `question()` checks `effectiveSignal?.aborted` synchronously before attaching the listener, preventing a TOCTOU where the signal is already aborted when `question()` is entered. The abort listener is removed via `removeEventListener` after `iter.next()` resolves to avoid memory leaks on long-lived interfaces.
|
||||
|
||||
**Shared iterator state** — `createScanner` is a single generator; both `question()` and the async iterator share the same `iter`. Callers must not mix `question()` calls and `for await` on the same interface concurrently — the semantics are sequential.
|
||||
Reference in New Issue
Block a user