dmxServeMacros function
Serves macros over the worker protocol until the driver closes stdin
dartmacros.protocol. This is the whole of main for a macro worker.
The frames are drained by a listener rather than an await for loop: an
expansion that awaits a render dartmacros.render is waiting on a frame
that has not arrived yet, and a loop suspended inside its own body would
never read it.
name and version are this worker's own identity in the handshake, not
dmx's — the driver reads neither, and they exist so a worker can say what
it is in a log or a --verbose trace. version defaults to the version of
the dmx package the worker was built against, which is generated from
pubspec.yaml rather than written down release.version.
Implementation
Future<void> dmxServeMacros(
List<DmxMacro> macros, {
String name = 'macros',
String version = DmxPackage.version,
}) async {
final byName = {for (final macro in macros) macro.name: macro};
final driver = _Driver();
final closed = Completer<void>();
// Expansions answer in the order they were asked for, whatever each one
// awaits along the way [dartmacros.pipeline].
var queue = Future<void>.value();
final frames = stdin.transform(utf8.decoder).transform(const LineSplitter());
frames.listen(
(frame) {
final Object? message = jsonDecode(frame);
if (message is! Map<String, Object?>) {
return;
}
switch (message['op']) {
case 'hello':
_reply({
'v': 1,
'name': name,
'version': version,
'contextVersion': 1,
'ops': ['expand'],
'macros': byName.keys.toList(),
});
case 'expand':
queue = queue.then(
(_) async => _reply(await _expand(byName, message, driver)),
);
default:
driver.settle(message);
}
},
onDone: () {
if (!closed.isCompleted) {
closed.complete();
}
},
);
await closed.future;
await queue;
}