# interface _(no submodules)_ ## 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` | 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` | 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.