exchange method

Future<Map<String, Object?>> exchange(
  1. Map<String, String> params
)

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;
  }
}