recv method

Waits for the next query.

Completes with:

  • RecvData as soon as a query is available — immediately if one is already buffered, otherwise when the next one arrives.
  • RecvDisconnected when the producing end goes: this queryable is closed, or its session closes. For a fifo, only after its buffered queries have been handed over first.

It never completes RecvEmpty, and it never hangs, for the same reason canon's own blocking recv is two-valued: it waits rather than reporting an empty buffer.

No thread is parked and no isolate is created — the native side signals readiness and this future consumes through the ordinary synchronous tryRecv.

Throws StateError if a recv() is already pending — one pull at a time per handle. An interleaved tryRecv is fine and wins: it reaches the channel first, and the pending recv() re-arms for the next arrival.

Throws StateError if this queryable has been closed.

Implementation

Future<RecvResult<Query>> recv() {
  if (_closed) throw StateError('PullQueryable has been closed');
  if (_pending != null) {
    throw StateError('a recv() is already pending on this PullQueryable');
  }

  final immediate = tryRecv();
  if (immediate is! RecvEmpty<Query>) {
    return Future<RecvResult<Query>>.value(immediate);
  }

  // REGISTER, THEN ARM, THEN LOOK AGAIN — the order closes the arm-vs-arrival
  // race. Dart is single-threaded, so no port message can be delivered into
  // the gap between the first look and the arming.
  final completer = Completer<RecvResult<Query>>();
  _pending = completer;
  bindings.zd_pull_tee_arm(_teeHandle);

  final afterArm = tryRecv();
  if (afterArm is! RecvEmpty<Query>) {
    _pending = null;
    // The arming stays set and the next delivery spends it on a single ping
    // that `_onWake` discards. Bounded at one, and self-clearing.
    return Future<RecvResult<Query>>.value(afterArm);
  }
  return completer.future;
}