exchange method
Serves one exchange: apply the incoming frame, if any, and drain that peer's queue. Public because it is the transport — a host that reaches the app some other way (an embedder, a test) drives this directly instead of going through the service extension.
The pause before draining is not a hedge, it is the common case.
InspectorCore answers a request from an async method, so even a
handler that returns a value immediately enqueues its response one
microtask after handleFrame returns. Draining synchronously would send
every command's caller away empty-handed and make it wait for a nudge and
a second round trip — measured on a real VM service before this existed:
EXPLAIN -> {"frames":[]}. A genuinely async handler still misses this
window and still answers on a later pull, which is the case the nudge is
for.
Implementation
Future<Map<String, Object?>> exchange(Map<String, String> params) async {
var id = params['peer'] ?? 'default';
if (params['detach'] == 'true') {
var peer = _peers.remove(id);
if (peer != null) core.detach(peer);
return const {'frames': <Object?>[], 'detached': true};
}
var peer = _peers.putIfAbsent(
id,
() => _QueuedPeer(id, queueLimit, _onQueued),
);
// Held across the pause, so a frame that lands during it is answered by
// this call rather than nudged about and then drained anyway.
_serving = peer;
try {
var encoded = params['frame'];
if (encoded != null) {
var frame = tryDecodeFrame(encoded);
if (frame != null) core.handleFrame(peer, frame);
}
await Future<void>.delayed(Duration.zero);
return peer.drain();
} finally {
_serving = null;
}
}