runAsync for-loop does not wait until loop body ends to print prompt
Take this sample program:
import 'package:cli_repl/cli_repl.dart';
void main() async {
final repl = Repl(prompt: '> ');
await for (final answer in repl.runAsync()) {
if (answer == 'exit') break;
await Future(() => print("You said '$answer'"));
}
print("done");
await repl.exit();
}
When running this program, the You said x message is always printed after the prompt. This makes writing REPLs that evaluate commands async impossible even when the for-loop body awaits, as in this example.
The expected behaviour is to wait for the body to complete before printing the prompt.
From what I can see, the current design makes it impossible for the desired behaviour to be used. That's a shame because the sync method blocks the event loop and causes tools like the observatory to become unusable, not to mention that scheduled async processes will also be inactive while the user does not interact with the shell.
I would propose a design change (unless a solution can be found with the current design - which I could not do) to make this library usable in "async mode"...
Instead of returning a Stream from runAsync, it should take a function, akin to forEach(), and the function should return whether the loop should continue or not, as in this simplified implementation:
import 'dart:async';
import 'dart:io';
const prompt = '> ';
enum NextAction { go_on, stop }
Future<void> runAsync(FutureOr<NextAction> Function(String) onLine) async {
var stream = stdin.expand((b) => b);
stdout.write(prompt);
final buf = StringBuffer();
await for (int i in stream) {
final c = String.fromCharCode(i);
stdout.write(c);
if (c == '\n') {
final action = await onLine(buf.toString());
buf.clear();
if (action == NextAction.stop) break;
stdout.write(prompt);
} else {
buf.write(c);
}
}
}
Now, this program will work as expected:
void main() async {
stdin.echoMode = false;
stdin.lineMode = false;
// user loop
await runAsync((line) async {
if (line == 'exit') return NextAction.stop;
await Future.delayed(Duration(seconds: 1));
print("You said $line");
return NextAction.go_on;
});
print("done");
}
This also fixes issue #2 .
From what I've seen, the few libraries depending on this package don't use runAsync, so I think a design change is acceptable given it's still in version 0.x. What do you think @jathak ?
Hi, sorry for the delay responding. I must have missed the notification email.
I'm open to a potential design change for this function, though it would need to be something that works on both the Dart VM (where it uses a pure-Dart reimplementation of readline) and Node (where it just uses the Node readline module).
I'll also note that Dart Sass uses runAsync, and it's by far the most widely used dependent of this package, so any changes made would need to make sure not to break it (on the Dart VM or on Node, since it uses both).
I don't really have any bandwidth to work on this right now, but if you'd like to work on a PR yourself, I'd be happy to review it.