stream property

Stream<Query> get stream

A bounded, demand-gated Stream view over this handle's own recv.

Unlike Session.declareQueryable's stream, which pushes every arrival into an unbounded StreamController seam, this one pulls: it takes one query at a time out of the bounded native channel, and only while the subscription is demanding.

This column is where the bound costs native memory, not only Dart

Every buffered query is a z_owned_query_t clone held on the native side until it is disposed. A slow consumer on the push stream therefore grows both the Dart queue and native memory; a slow consumer here retains the channel's own capacity and at most one already-pulled query — at most capacity + 1, because the first pull starts synchronously inside onListen and the query completing it goes to a one-slot stash.

⚠️ A ceiling, not an equality. Measured on ring, capacity: 8: 64 concurrent getters left 8 queries retained where the shipped push queryable retained all 64. Canon's ring makes room before it inserts, so sustained overflow sits one short of the ceiling.

What happens to the queries that do not fit is the channel's kind, unchanged: a ChannelKind.ring drops its oldest channel entry, which the requester sees as its getter finalizing with no reply; a ChannelKind.fifo stalls inbound delivery instead.

⚠️ This getter is a MODE SWITCH

Once the returned stream has a listener, the drive loop owns this handle's recv: calling recv yourself throws StateError while a pull is in flight, and an interleaved tryRecv competes with the loop for arrivals. Pick one consumption idiom per handle.

A query that completed a pull into a paused or cancelled subscription is stashed, not disposed: tryRecv hands it back before it touches the channel — so it can still be replied to — and close disposes anything still held, so the remedy does not trade a lost reply for a leak.

Reply to every Query you take and then call Query.dispose, exactly as on the polling path.

⚠️ Capacity 0, measured per kind — this column only

On a ChannelKind.fifo at capacity 0 this stream delivers nothing; on ChannelKind.ring it delivers. ⚠️ That is not what Session.declarePullQueryable reports for the same capacity, and both are true: that dartdoc says both kinds hand a query over under polling, which is about tryRecv. This stream does not poll — it drives recv, and recv is the one accessor a capacity-0 fifo cannot serve on either column, because the rendezvous is full when it is empty. The axis is the accessor, not the column.

Teardown

With a pull still in flight, close completes it and the loop's own terminal arm closes the stream; with the loop already exited holding a stash, only close's gate step can. Either way anything stashed is disposed there — measured, the requester's getter then finalizes in ~24 ms instead of waiting out its own timeout.

Single-subscription. Throws StateError if this queryable has been closed, exactly as tryRecv and recv do.

Implementation

Stream<Query> get stream {
  if (_closed) throw StateError('PullQueryable has been closed');
  return (_gate ??= DemandGate<Query>(
    pull: recv,
    // ⚠️ NOT a no-op on this column. A query the consumer will never see
    // still holds a native clone, and dropping it is what sends canon's
    // `ResponseFinal` -- so the requester's getter completes promptly
    // instead of waiting out its own timeout.
    release: (query) => query.dispose(),
  )).stream;
}