Skip to main content

nodejs/stdlib/
readline.rs

1//! Node `readline` module.
2//!
3//! * `'line'` and `'close'` events, and `for await (const line of rl)`: an
4//!   Interface listens to its input stream's `'data'`/`'end'` — from creation,
5//!   or for `process.stdin` from the first `'line'`/`'close'` listener or async
6//!   iterator, which sets standard input flowing (`process::stdin_call`). Lines split
7//!   on `\r\n`, `\n` or a lone `\r`, an unterminated last line is delivered at
8//!   end of input, then `'close'`. See "line events" below for the ordering.
9//! * `interface.question(query, cb)` writes `query` to stdout and, before any
10//!   line events have started, reads exactly ONE line from stdin synchronously
11//!   and invokes `cb(line)`; once they have, the answer is the next line the
12//!   input delivers. `readline/promises` resolves a promise instead.
13//! * `interface.write(data)` writes to the output; `prompt()` writes the stored
14//!   prompt; `setPrompt`/`getPrompt` manage it; the module cursor helpers
15//!   (`cursorTo`/`moveCursor`/`clearLine`/`clearScreenDown`) emit the
16//!   corresponding ANSI control sequences to stdout.
17//! * Not modelled: terminal mode (keypress decoding, history, line editing) —
18//!   input is read as lines whatever it is attached to — and `pause()`/
19//!   `resume()`, which return the Interface and change nothing.
20//!
21//! An Interface is a plain object tagged `@@native = "Interface"` carrying the
22//! passed `@@input`/`@@output` streams, the current `@@prompt`, a hidden
23//! `@@listeners` object of registered event callbacks, and the `@@rlid` that
24//! keys its line state.
25
26use crate::host::{is_callable, with_host, JsObj};
27use fusevm::Value;
28use indexmap::IndexMap;
29use std::collections::{HashMap, VecDeque};
30use std::io::{self, Write};
31
32pub const METHODS: &[&str] = &[
33    "createInterface",
34    "clearLine",
35    "clearScreenDown",
36    "cursorTo",
37    "moveCursor",
38    "emitKeypressEvents",
39];
40
41/// Methods dispatched on an `@@native = "Interface"` object (reported to the
42/// parent for `instance_has_method` wiring).
43pub const INTERFACE_METHODS: &[&str] = &[
44    "question",
45    "write",
46    "close",
47    "pause",
48    "resume",
49    "prompt",
50    "setPrompt",
51    "getPrompt",
52    "on",
53    "once",
54    "addListener",
55    "prependListener",
56    "removeListener",
57    "off",
58    "removeAllListeners",
59    "@@asyncIterator",
60];
61
62/// The `readline/promises` surface: the same module, whose `createInterface`
63/// builds an Interface with a promise-returning `question`.
64pub const PROMISES_METHODS: &[&str] = &["createInterface"];
65
66pub fn call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
67    if method.starts_with("@@") {
68        return internal_call(method, args);
69    }
70    Some(match method {
71        "createInterface" => Ok(create_interface(args, false)),
72        // Cursor / line control: emit the ANSI sequence to stdout. Node passes the
73        // target stream as the first arg; node-js writes to the real stdout (the
74        // usual `process.stdout` target). Each returns `true` (write accepted).
75        "cursorTo" => {
76            let x = super::arg_num(args, 1);
77            let y = args.get(2).filter(|v| !matches!(v, Value::Undef));
78            let seq = match y {
79                Some(yv) => format!(
80                    "\x1b[{};{}H",
81                    with_host(|h| h.to_number(yv)) as i64 + 1,
82                    x as i64 + 1
83                ),
84                None => format!("\x1b[{}G", x as i64 + 1),
85            };
86            write_stdout(&seq);
87            Ok(Value::Bool(true))
88        }
89        "moveCursor" => {
90            let dx = super::arg_num(args, 1) as i64;
91            let dy = super::arg_num(args, 2) as i64;
92            let mut seq = String::new();
93            if dx > 0 {
94                seq.push_str(&format!("\x1b[{dx}C"));
95            } else if dx < 0 {
96                seq.push_str(&format!("\x1b[{}D", -dx));
97            }
98            if dy > 0 {
99                seq.push_str(&format!("\x1b[{dy}B"));
100            } else if dy < 0 {
101                seq.push_str(&format!("\x1b[{}A", -dy));
102            }
103            write_stdout(&seq);
104            Ok(Value::Bool(true))
105        }
106        "clearLine" => {
107            let dir = super::arg_num(args, 1);
108            // dir < 0 → to start (1K); dir > 0 → to end (0K); 0 → whole line (2K).
109            let seq = if dir < 0.0 {
110                "\x1b[1K"
111            } else if dir > 0.0 {
112                "\x1b[0K"
113            } else {
114                "\x1b[2K"
115            };
116            write_stdout(seq);
117            Ok(Value::Bool(true))
118        }
119        "clearScreenDown" => {
120            write_stdout("\x1b[0J");
121            Ok(Value::Bool(true))
122        }
123        // `readline.emitKeypressEvents(stream)` normally attaches an input decoder
124        // that makes `stream` emit `'keypress'` events. node-js has no background
125        // TTY reader driving async input events (see the module docs), so there is
126        // nothing to attach: an honest no-op rather than a fake key stream.
127        "emitKeypressEvents" => Ok(Value::Undef),
128        _ => return None,
129    })
130}
131
132/// `new readline.Interface(options | input[, output])` — the class form of
133/// `createInterface`, producing the same `@@native = "Interface"` object.
134/// Requires the parent to route `"Interface"` construction into this fn.
135pub fn construct(args: &[Value]) -> Result<Value, String> {
136    Ok(create_interface(args, false))
137}
138
139/// A non-function member of the `readline` namespace (reachable via
140/// `namespace_property` IF the parent routes `"readline"` into `stdlib::constant`).
141/// `readline.Interface` is the interface constructor.
142pub fn constant(name: &str) -> Option<Value> {
143    match name {
144        "Interface" => Some(with_host(|h| h.alloc(JsObj::Builtin("Interface".into())))),
145        _ => None,
146    }
147}
148
149/// `readline.createInterface(options | input[, output])` → an Interface object.
150/// `require('readline/promises').<method>`.
151pub fn promises_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
152    match method {
153        "createInterface" => Some(Ok(create_interface(args, true))),
154        _ => None,
155    }
156}
157
158fn create_interface(args: &[Value], promises: bool) -> Value {
159    let id = NEXT_ID.with(|n| {
160        let id = n.get();
161        n.set(id + 1);
162        id
163    });
164    // Options object form `{ input, output }` vs positional `(input, output)`.
165    let (input, output) = match args.first() {
166        Some(o) if opt_prop(o, "input").is_some() => (
167            opt_prop(o, "input").unwrap_or(Value::Undef),
168            opt_prop(o, "output").unwrap_or(Value::Undef),
169        ),
170        _ => (
171            args.first().cloned().unwrap_or(Value::Undef),
172            args.get(1).cloned().unwrap_or(Value::Undef),
173        ),
174    };
175    let iface = with_host(|h| {
176        let listeners = h.new_object(IndexMap::new());
177        let prompt = h.new_str("> ");
178        let mut m = IndexMap::new();
179        m.insert("@@native".into(), h.new_str("Interface"));
180        m.insert("@@input".into(), input.clone());
181        m.insert("@@output".into(), output);
182        m.insert("@@prompt".into(), prompt);
183        m.insert("@@listeners".into(), listeners);
184        m.insert("@@rlid".into(), Value::Float(id as f64));
185        if promises {
186            let flag = h.new_str("1");
187            m.insert("@@promises".into(), flag);
188        }
189        h.new_object(m)
190    });
191    if matches!(input, Value::Obj(_)) && !is_stdin(&input) {
192        attach_stream(&iface, &input);
193    }
194    iface
195}
196
197/// Dispatch a method on an Interface instance (`@@native = "Interface"`).
198pub fn instance_call(recv: &Value, method: &str, args: Vec<Value>) -> Result<Value, String> {
199    match method {
200        // REAL synchronous single-line read: write the query, read one stdin line,
201        // invoke the callback with it. Returns undefined (Node's callback form).
202        "question" => {
203            let query = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
204            write_stdout(&query);
205            // Once input is flowing as line events, the answer is the next
206            // line the stream delivers rather than a read of its own.
207            if with_state(recv, |s| s.flowing) {
208                if read_hidden(recv, "@@promises") == "1" {
209                    let (promise, id) = with_host(|h| {
210                        let p = h.new_promise();
211                        let id = h.promise_id(&p).unwrap_or(0);
212                        (p, id)
213                    });
214                    with_state(recv, |s| s.question = Some(QuestionReply::Promise(id)));
215                    return Ok(promise);
216                }
217                let cb = args
218                    .iter()
219                    .rev()
220                    .find(|v| with_host(|h| is_callable(h, v)))
221                    .cloned();
222                if let Some(cb) = cb {
223                    with_state(recv, |s| s.question = Some(QuestionReply::Callback(cb)));
224                }
225                return Ok(Value::Undef);
226            }
227            let line = read_line();
228            // `require('readline/promises')` builds an Interface whose
229            // `question` RESOLVES with the line instead of taking a callback.
230            // The two forms are the same read; only the handoff differs.
231            if read_hidden(recv, "@@promises") == "1" {
232                let line_val = with_host(|h| h.new_str(line));
233                return crate::builtins::promise_resolve_pub(line_val);
234            }
235            // The callback is the last callable argument (Node: `question(q, cb)`
236            // or `question(q, options, cb)`).
237            // The `.find` predicate receives `&&Value`; deref once so `is_callable`
238            // sees a `&Value`. `find` yields `Option<&Value>`, cloned to `Value`.
239            let cb = args
240                .iter()
241                .rev()
242                .find(|v| with_host(|h| is_callable(h, v)))
243                .cloned();
244            if let Some(cb) = cb {
245                let line_val = with_host(|h| h.new_str(line));
246                crate::host::invoke(&cb, vec![line_val], None)?;
247            }
248            Ok(Value::Undef)
249        }
250        "write" => {
251            let data = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
252            write_output(recv, &data);
253            Ok(Value::Undef)
254        }
255        "prompt" => {
256            let p = read_hidden(recv, "@@prompt");
257            write_output(recv, &p);
258            Ok(Value::Undef)
259        }
260        "setPrompt" => {
261            let p = with_host(|h| args.first().map(|v| h.str_of(v)).unwrap_or_default());
262            with_host(|h| {
263                let pv = h.new_str(p);
264                if let Some(JsObj::Object(m)) = h.get_mut(recv) {
265                    m.insert("@@prompt".into(), pv);
266                }
267            });
268            Ok(Value::Undef)
269        }
270        // `read_hidden` takes the host, so reading it INSIDE `with_host` borrowed
271        // the same RefCell twice and aborted the process — a Rust panic, not a
272        // throw, so no JS `try` could catch it. Read first, then borrow.
273        "getPrompt" => {
274            let prompt = read_hidden(recv, "@@prompt");
275            Ok(with_host(|h| h.new_str(prompt)))
276        }
277        // Listener registration, under `@@listeners[event]`. A `'line'` or
278        // `'close'` listener on standard input sets it flowing.
279        "on" | "once" | "addListener" | "prependListener" => {
280            if let (Some(ev), Some(cb)) = (args.first(), args.get(1)) {
281                let event = with_host(|h| h.str_of(ev));
282                store_listener(
283                    recv,
284                    &event,
285                    cb.clone(),
286                    method == "once",
287                    method == "prependListener",
288                );
289                if event == "line" || event == "close" {
290                    ensure_flowing(recv);
291                }
292            }
293            Ok(recv.clone())
294        }
295        "removeListener" | "off" => {
296            if let (Some(ev), Some(cb)) = (args.first(), args.get(1)) {
297                let event = with_host(|h| h.str_of(ev));
298                remove_listener(recv, &event, cb);
299            }
300            Ok(recv.clone())
301        }
302        "removeAllListeners" => {
303            let event = args
304                .first()
305                .filter(|v| !matches!(v, Value::Undef))
306                .map(|v| with_host(|h| h.str_of(v)));
307            clear_listeners(recv, event.as_deref());
308            Ok(recv.clone())
309        }
310        "close" => {
311            close(recv)?;
312            Ok(Value::Undef)
313        }
314        "@@asyncIterator" => Ok(async_iterator(recv)),
315        // Reading is driven by the input stream's own events; there is no
316        // separate flow switch to flip here.
317        "pause" | "resume" => Ok(recv.clone()),
318        _ => Err(crate::host::type_error(&format!(
319            "{method} is not a function"
320        ))),
321    }
322}
323
324/// Read the `key` hidden string property of `recv`.
325fn read_hidden(recv: &Value, key: &str) -> String {
326    with_host(|h| match h.get(recv) {
327        Some(JsObj::Object(p)) => p.get(key).map(|v| h.str_of(v)).unwrap_or_default(),
328        _ => String::new(),
329    })
330}
331
332/// The `@@listeners` object of `recv`.
333fn listeners_obj(recv: &Value) -> Option<Value> {
334    opt_prop(recv, "@@listeners")
335}
336
337/// Add `cb` to `recv`'s `@@listeners[event]`, an array of `[callback, once]`
338/// pairs (created on demand); `prepend` puts it first.
339fn store_listener(recv: &Value, event: &str, cb: Value, once: bool, prepend: bool) {
340    let Some(listeners) = listeners_obj(recv) else {
341        return;
342    };
343    with_host(|h| {
344        let entry = h.new_array(vec![cb, Value::Bool(once)]);
345        let arr = match h.get(&listeners) {
346            Some(JsObj::Object(p)) => p.get(event).cloned(),
347            _ => None,
348        };
349        let arr = arr.filter(|a| matches!(h.get(a), Some(JsObj::Array(_))));
350        match arr {
351            Some(a) => {
352                if let Some(JsObj::Array(items)) = h.get_mut(&a) {
353                    if prepend {
354                        items.insert(0, entry);
355                    } else {
356                        items.push(entry);
357                    }
358                }
359            }
360            None => {
361                let a = h.new_array(vec![entry]);
362                if let Some(JsObj::Object(p)) = h.get_mut(&listeners) {
363                    p.insert(event.to_string(), a);
364                }
365            }
366        }
367    });
368}
369
370/// The `(callback, once)` pairs registered for `event`, in call order.
371fn listener_entries(recv: &Value, event: &str) -> Vec<(Value, bool)> {
372    let Some(listeners) = listeners_obj(recv) else {
373        return Vec::new();
374    };
375    with_host(|h| {
376        let arr = match h.get(&listeners) {
377            Some(JsObj::Object(p)) => p.get(event).cloned(),
378            _ => None,
379        };
380        let Some(Some(JsObj::Array(items))) = arr.map(|a| h.get(&a).cloned()) else {
381            return Vec::new();
382        };
383        items
384            .iter()
385            .filter_map(|e| match h.get(e) {
386                Some(JsObj::Array(pair)) if pair.len() == 2 => {
387                    Some((pair[0].clone(), h.truthy(&pair[1])))
388                }
389                _ => None,
390            })
391            .collect()
392    })
393}
394
395/// Remove the first registration of `cb` for `event`.
396fn remove_listener(recv: &Value, event: &str, cb: &Value) {
397    let Some(listeners) = listeners_obj(recv) else {
398        return;
399    };
400    with_host(|h| {
401        let arr = match h.get(&listeners) {
402            Some(JsObj::Object(p)) => p.get(event).cloned(),
403            _ => None,
404        };
405        let Some(arr) = arr else { return };
406        let pos = match h.get(&arr) {
407            Some(JsObj::Array(items)) => items.iter().position(|e| match h.get(e) {
408                Some(JsObj::Array(pair)) => pair.first().is_some_and(|f| h.strict_eq(f, cb)),
409                _ => false,
410            }),
411            _ => None,
412        };
413        if let (Some(i), Some(JsObj::Array(items))) = (pos, h.get_mut(&arr)) {
414            items.remove(i);
415        }
416    });
417}
418
419/// `removeAllListeners([event])`.
420fn clear_listeners(recv: &Value, event: Option<&str>) {
421    let Some(listeners) = listeners_obj(recv) else {
422        return;
423    };
424    with_host(|h| {
425        if let Some(JsObj::Object(p)) = h.get_mut(&listeners) {
426            match event {
427                Some(e) => {
428                    p.shift_remove(e);
429                }
430                None => p.clear(),
431            }
432        }
433    });
434}
435
436/// Read one line from stdin, stripping the trailing CR/LF. EOF yields "".
437fn read_line() -> String {
438    let mut line = String::new();
439    let _ = io::stdin().read_line(&mut line);
440    while line.ends_with('\n') || line.ends_with('\r') {
441        line.pop();
442    }
443    line
444}
445
446/// Write `s` to real stdout and flush (this is explicit program output — a
447/// readline prompt / write — not informational chatter).
448fn write_stdout(s: &str) {
449    let mut out = io::stdout();
450    let _ = out.write_all(s.as_bytes());
451    let _ = out.flush();
452}
453
454/// Write to the interface's configured `output` stream, falling back to stdout
455/// when it has none.
456///
457/// `createInterface({input, output})` records `@@output` and Node writes
458/// `prompt()` and `write()` THROUGH it — the option exists so a caller can
459/// capture or redirect that text. Both went straight to `io::stdout()` here, so
460/// an interface given its own output printed to the process's stdout anyway and
461/// the supplied stream never saw a byte.
462fn write_output(recv: &Value, s: &str) {
463    let out = opt_prop(recv, "@@output").unwrap_or(Value::Undef);
464    if matches!(out, Value::Obj(_)) {
465        let payload = with_host(|h| h.new_str(s.to_string()));
466        if crate::host::call_method(&out, "write", vec![payload]).is_ok() {
467            return;
468        }
469    }
470    write_stdout(s);
471}
472
473/// An own property of `v` if `v` is a plain object, else `None`.
474fn opt_prop(v: &Value, key: &str) -> Option<Value> {
475    with_host(|h| match h.get(v) {
476        Some(JsObj::Object(p)) => p.get(key).cloned(),
477        _ => None,
478    })
479}
480
481// ── line events ──────────────────────────────────────────────────────────────
482//
483// An Interface splits its input into lines and emits `'line'` for each, then
484// `'close'` when the input ends, as node's does. It listens to its input
485// stream's `'data'`/`'end'` (standard input reads through the same Rust buffer
486// `question` reads, so the two never skip each other's bytes). A chunk's lines
487// are emitted synchronously, one after another, as node emits them.
488
489/// Per-Interface line state, keyed by the Interface's `@@rlid`.
490#[derive(Default)]
491struct LineState {
492    /// Bytes received but not yet ended by a line break.
493    pending: Vec<u8>,
494    /// The last chunk ended in `\r`, so a `\n` opening the next one is part of
495    /// that line break rather than an empty line.
496    after_cr: bool,
497    /// Complete lines not yet emitted.
498    to_emit: VecDeque<String>,
499    /// A chain of line emissions is in progress.
500    emitting: bool,
501    /// The input has ended: `'close'` follows the last line.
502    ended: bool,
503    closed: bool,
504    /// The input is flowing into this Interface (always, for a stream input;
505    /// from the first listener, for standard input).
506    flowing: bool,
507    /// Async iteration: lines no `next()` has taken yet, and the promises of
508    /// the `next()` calls waiting for one.
509    iterating: bool,
510    buffered: VecDeque<String>,
511    waiters: VecDeque<u32>,
512    /// A `question` waiting for the next line: its callback, or (for
513    /// `readline/promises`) the id of the promise it returned.
514    question: Option<QuestionReply>,
515}
516
517enum QuestionReply {
518    Callback(Value),
519    Promise(u32),
520}
521
522thread_local! {
523    static LINES: std::cell::RefCell<HashMap<u64, LineState>> =
524        std::cell::RefCell::new(HashMap::new());
525    static NEXT_ID: std::cell::Cell<u64> = const { std::cell::Cell::new(1) };
526}
527
528fn rl_id(recv: &Value) -> u64 {
529    opt_prop(recv, "@@rlid")
530        .map(|v| with_host(|h| h.to_number(&v)) as u64)
531        .unwrap_or(0)
532}
533
534fn with_state<R>(recv: &Value, f: impl FnOnce(&mut LineState) -> R) -> R {
535    let id = rl_id(recv);
536    LINES.with(|m| f(m.borrow_mut().entry(id).or_default()))
537}
538
539/// Whether `input` is `process.stdin`.
540fn is_stdin(input: &Value) -> bool {
541    with_host(|h| match h.get(input) {
542        Some(JsObj::Object(p)) => {
543            p.get("@@native").map(|v| h.str_of(v)).as_deref() == Some("WriteStream")
544                && p.get("fd").map(|v| h.to_number(v)) == Some(0.0)
545        }
546        _ => false,
547    })
548}
549
550/// Start reading standard input, once, if that is this Interface's input.
551fn ensure_flowing(recv: &Value) {
552    let input = opt_prop(recv, "@@input").unwrap_or(Value::Undef);
553    if !is_stdin(&input) {
554        return;
555    }
556    let start = with_state(recv, |s| {
557        !std::mem::replace(&mut s.flowing, true) && !s.closed
558    });
559    if start {
560        attach_stream(recv, &input);
561    }
562}
563
564/// Split a chunk into lines on `\r\n`, `\n` or a lone `\r`, keeping the
565/// unterminated tail for the next chunk.
566fn on_data(recv: &Value, bytes: &[u8]) -> Result<(), String> {
567    // A closed Interface has stopped listening to its input.
568    if with_state(recv, |s| s.closed) {
569        return Ok(());
570    }
571    let start = with_state(recv, |s| {
572        let mut i = 0;
573        if s.after_cr && bytes.first() == Some(&b'\n') {
574            i = 1;
575        }
576        s.after_cr = false;
577        while i < bytes.len() {
578            match bytes[i] {
579                b'\n' => {
580                    let line = String::from_utf8_lossy(&s.pending).into_owned();
581                    s.pending.clear();
582                    s.to_emit.push_back(line);
583                }
584                b'\r' => {
585                    let line = String::from_utf8_lossy(&s.pending).into_owned();
586                    s.pending.clear();
587                    s.to_emit.push_back(line);
588                    match bytes.get(i + 1) {
589                        Some(b'\n') => i += 1,
590                        None => s.after_cr = true,
591                        _ => {}
592                    }
593                }
594                b => s.pending.push(b),
595            }
596            i += 1;
597        }
598        !s.to_emit.is_empty() && !std::mem::replace(&mut s.emitting, true)
599    });
600    if start {
601        emit_lines(recv)?;
602    }
603    Ok(())
604}
605
606/// The input ended: what is left unterminated is the last line, then `'close'`.
607fn on_end(recv: &Value) -> Result<(), String> {
608    let (start, close_now) = with_state(recv, |s| {
609        s.ended = true;
610        if !s.pending.is_empty() {
611            let line = String::from_utf8_lossy(&s.pending).into_owned();
612            s.pending.clear();
613            s.to_emit.push_back(line);
614        }
615        let start = !s.to_emit.is_empty() && !std::mem::replace(&mut s.emitting, true);
616        (start, s.to_emit.is_empty() && !s.emitting)
617    });
618    if start {
619        emit_lines(recv)?;
620    } else if close_now {
621        close(recv)?;
622    }
623    Ok(())
624}
625
626/// Emit every queued line, in order and synchronously — node emits a chunk's
627/// lines in one go, so the microtasks a `'line'` listener queues run after the
628/// whole chunk — then `'close'` if the input has ended. A line arriving while
629/// this runs (a listener feeding the input) joins the same loop.
630fn emit_lines(recv: &Value) -> Result<(), String> {
631    while let Some(line) = with_state(recv, |s| s.to_emit.pop_front()) {
632        if let Err(e) = deliver(recv, line) {
633            with_state(recv, |s| s.emitting = false);
634            return Err(e);
635        }
636    }
637    let close_now = with_state(recv, |s| {
638        s.emitting = false;
639        s.ended
640    });
641    if close_now {
642        close(recv)?;
643    }
644    Ok(())
645}
646
647/// Hand one line to a waiting `question`, or else to the `'line'` listeners
648/// and any async iterator.
649fn deliver(recv: &Value, line: String) -> Result<(), String> {
650    match with_state(recv, |s| s.question.take()) {
651        Some(QuestionReply::Callback(cb)) => {
652            let v = with_host(|h| h.new_str(line));
653            crate::host::invoke(&cb, vec![v], None)?;
654            return Ok(());
655        }
656        Some(QuestionReply::Promise(id)) => {
657            let v = with_host(|h| h.new_str(line));
658            crate::host::resolve_promise_val(id, v);
659            return Ok(());
660        }
661        None => {}
662    }
663    let v = with_host(|h| h.new_str(line.clone()));
664    emit(recv, "line", vec![v.clone()])?;
665    let waiter = with_state(recv, |s| {
666        if !s.iterating {
667            return None;
668        }
669        let w = s.waiters.pop_front();
670        if w.is_none() {
671            s.buffered.push_back(line);
672        }
673        w
674    });
675    if let Some(id) = waiter {
676        crate::host::resolve_promise_val(id, iter_result(v, false));
677    }
678    Ok(())
679}
680
681/// `rl.close()`, and the end of input: emit `'close'` once and finish any
682/// async iteration.
683fn close(recv: &Value) -> Result<(), String> {
684    let waiters = with_state(recv, |s| {
685        if std::mem::replace(&mut s.closed, true) {
686            return None;
687        }
688        Some(std::mem::take(&mut s.waiters))
689    });
690    let Some(waiters) = waiters else {
691        return Ok(());
692    };
693    // A closed Interface stops its input, so standard input no longer holds
694    // the process open.
695    let input = opt_prop(recv, "@@input").unwrap_or(Value::Undef);
696    if is_stdin(&input) {
697        crate::host::call_method(&input, "pause", Vec::new())?;
698    }
699    emit(recv, "close", Vec::new())?;
700    for id in waiters {
701        crate::host::resolve_promise_val(id, iter_result(Value::Undef, true));
702    }
703    Ok(())
704}
705
706/// Call the listeners registered for `event`, `once` ones removed first.
707fn emit(recv: &Value, event: &str, args: Vec<Value>) -> Result<(), String> {
708    let entries = listener_entries(recv, event);
709    for (cb, once) in entries {
710        if once {
711            remove_listener(recv, event, &cb);
712        }
713        crate::host::invoke(&cb, args.clone(), Some(recv.clone()))?;
714    }
715    Ok(())
716}
717
718/// `{ value, done }`.
719fn iter_result(value: Value, done: bool) -> Value {
720    with_host(|h| {
721        let mut m = IndexMap::new();
722        m.insert("value".into(), value);
723        m.insert("done".into(), Value::Bool(done));
724        h.new_object(m)
725    })
726}
727
728/// Module-internal entry points reached through `readline.@@…` builtins: the
729/// listeners attached to the input stream, and the deferred `close`.
730fn internal_call(method: &str, args: &[Value]) -> Option<Result<Value, String>> {
731    let recv = args.first().cloned().unwrap_or(Value::Undef);
732    let r = match method {
733        "@@feed" => {
734            let chunk = args.get(1).cloned().unwrap_or(Value::Undef);
735            let bytes = super::buffer::view_bytes(&chunk)
736                .unwrap_or_else(|| with_host(|h| h.str_of(&chunk)).into_bytes());
737            on_data(&recv, &bytes)
738        }
739        "@@end" => on_end(&recv),
740        "@@close" => close(&recv),
741        _ => return None,
742    };
743    Some(r.map(|_| Value::Undef))
744}
745
746/// Listen to a stream input's `'data'` and `'end'`, as node's Interface does
747/// from the moment it is created.
748fn attach_stream(recv: &Value, input: &Value) {
749    with_state(recv, |s| s.flowing = true);
750    let has_on = crate::builtins::get_property(input, "on")
751        .map(|f| with_host(|h| is_callable(h, &f)))
752        .unwrap_or(false);
753    if !has_on {
754        return;
755    }
756    for (event, internal) in [("data", "readline.@@feed"), ("end", "readline.@@end")] {
757        let (name, cb) = with_host(|h| {
758            let target = h.alloc(JsObj::Builtin(internal.into()));
759            let undef = Value::Undef;
760            let cb = h.alloc(JsObj::BoundFunc {
761                target,
762                this: undef,
763                args: vec![recv.clone()],
764            });
765            (h.new_str(event), cb)
766        });
767        let _ = crate::host::call_method(input, "on", vec![name, cb]);
768    }
769}
770
771/// The async iterator `for await (const line of rl)` reads: it takes the lines
772/// emitted from the moment it is created, and ends at `'close'`. Leaving the
773/// loop early (`return()`) closes the Interface, as in node.
774pub const ITERATOR_METHODS: &[&str] = &["next", "return", "@@asyncIterator"];
775
776fn async_iterator(recv: &Value) -> Value {
777    with_state(recv, |s| s.iterating = true);
778    ensure_flowing(recv);
779    with_host(|h| {
780        let mut m = IndexMap::new();
781        m.insert("@@native".into(), h.new_str("ReadlineIterator"));
782        m.insert("@@iface".into(), recv.clone());
783        h.new_object(m)
784    })
785}
786
787pub fn iterator_call(recv: &Value, method: &str) -> Result<Value, String> {
788    let iface = opt_prop(recv, "@@iface").unwrap_or(Value::Undef);
789    match method {
790        "@@asyncIterator" => Ok(recv.clone()),
791        "next" => {
792            let ready = with_state(&iface, |s| match s.buffered.pop_front() {
793                Some(line) => Some(Some(line)),
794                None if s.closed => Some(None),
795                None => None,
796            });
797            match ready {
798                Some(Some(line)) => {
799                    let v = with_host(|h| h.new_str(line));
800                    crate::builtins::promise_resolve_pub(iter_result(v, false))
801                }
802                Some(None) => crate::builtins::promise_resolve_pub(iter_result(Value::Undef, true)),
803                None => {
804                    let (promise, id) = with_host(|h| {
805                        let p = h.new_promise();
806                        let id = h.promise_id(&p).unwrap_or(0);
807                        (p, id)
808                    });
809                    with_state(&iface, |s| s.waiters.push_back(id));
810                    Ok(promise)
811                }
812            }
813        }
814        // Leaving the loop closes the Interface on the next tick, so the code
815        // after the loop runs before `'close'` fires, as in node.
816        "return" => {
817            with_host(|h| {
818                let cb = h.alloc(JsObj::Builtin("readline.@@close".into()));
819                h.queue_nexttick(cb, vec![iface.clone()]);
820            });
821            crate::builtins::promise_resolve_pub(iter_result(Value::Undef, true))
822        }
823        _ => Err(crate::host::type_error(&format!(
824            "{method} is not a function"
825        ))),
826    }
827}