- Split dispatch() into dispatch_start() (sync prefix) + spawned
invocation: both single-stream loops (serve_single_stream and the
accept-side run_loop_single_stream) spawn Once invocations, Sub
pumps, and sink response writers; only the Pub sink start stays
inline (chunk_tx must register before the next call.published).
Inline dispatch deadlocked same-connection nested composition: the
read loop awaited the parent handler, which awaited a nested call
whose response only the same read loop could resolve (resolved only
via the 30s sweeper). Spawned handles tracked + aborted at loop exit;
in_flight_sinks behind an Arc<parking_lot::Mutex> with guards dropped
before awaits.
- run_loop_single_stream gains the pending-resolution arms
(RESPONDED/COMPLETED/ERROR): the accept side previously served only
and had no loop resolving its own outbound pendings in single-stream
mode — the latent accept-side imported-op composition hazard is
mechanized shut.
- Write-failure in the spawned Once path warns instead of closing the
loop (matches the Sink arm; dying transport still surfaces via
ConnectionClosed on the next read).
- G-02 gate: hub_handler_composes_peer_announced_op_via_nested_composition
— announce -> consumer calls hub/compose -> hub's serving loop
wire-dispatches it -> handler composes via ctx.env -> forwarding
stub's nested call crosses back to the consumer. The F-05 gate
bypassed this path entirely.
- Interleaved-directions gate: outbound_call_resolves_while_inbound_
subscription_is_being_served — consumer serves a live Sub while a
wire-dispatched hub handler issues an outbound call on the same
connection.
- Both gates verified load-bearing: run against the pre-fix loop each
reproduces the G-01 hang (no progress, bounded-timeout failure);
post-fix both resolve in <0.2s, no sweeper evictions.
Verification: cargo test 583 / --all-features 600, clippy
(all-targets, all-features, wasm32) clean, fmt clean, doc clean.
Refs docs/reviews/005-...md (G-01, G-02; Units 2-3 open).