Expand description
One exec’s output as two byte channels, stdout and stderr.
§One drive, one cursor, two channels
The daemon publishes stdout and stderr into one SSE stream with a stream
discriminator per output frame, sharing one offset space. That’s what makes the byte
cursor work: a single cursor can’t be split into two without inventing an ordering between
them that the wire never stated. So ExecHandle::split runs one
ExecHandle::for_each_event_async drive, reconnects and all, and routes each output
frame to its side’s channel. Order is kept within each side and isn’t recoverable between
them; a caller that needs the interleaving reads ExecHandle::stream_with instead.
Both bindings’ process shapes (Node’s and Python’s Session.spawn) read these channels,
so the routing, the gap attribution and the gap policy are written once, here.
§A gap is attributed to the next output frame’s stream
A gap frame carries no discriminator: the offset space is shared, so a gap is a hole in
the combined stream and the daemon can’t say which side’s bytes were in it. The bytes that
resumed after the hole came out of one side, and that side’s log is the one with a hole in
it, so a gap is held until the next output frame and takes its stream. A gap the stream
ends on has nothing after it, and is recorded with no stream rather than guessed at.
§The gap policy
GapPolicy::Error, the default, ends both channels with an
WireKind::OutputGap error naming the range: both, because the shared offset space can’t
say which side lost the bytes, and erroring one would leave the other looking complete when
it may be the truncated one. An error can’t be ignored by the obvious consumer, a loop over
one side’s chunks, and a swallowed gap would hand it a contiguous-looking stream that’s
missing bytes. GapPolicy::Event records the gap in Split::gaps and keeps both
channels open, for a caller that wants the surviving bytes more than the completeness
guarantee (a log tail, a progress display).
StreamOptions::error_on_gap is ignored here and always off: a drive that ended on the
gap would lose the range’s attribution and never fill Split::gaps. The gap arrives as
an event and the policy is applied one layer up, in this module’s drive.
§One chunk per channel
Each channel holds one chunk, for the reason exec.rs gives: the daemon’s SSE body is the
backpressure signal, and buffering here would defeat the cursor the drive reconnects at.
Per channel rather than shared, so a slow stderr reader doesn’t hold chunks stdout has
already been handed. A channel nobody reads still stalls the drive once it holds its one
chunk, the same bound a pipe has, so a caller reads both sides concurrently or drops the
one it doesn’t want. Dropping either receiver ends the drive, since both share it.
Structs§
- GapLog
- The gaps a split drive has recorded so far, shared with the drive.
- Output
Gap - One byte range the daemon couldn’t replay, under
GapPolicy::Event. - Split
- One exec’s output as two channels, and the drive that fills them.
Enums§
- GapPolicy
- What a split drive does when the daemon reports evicted output. See the module docs.
Type Aliases§
- Split
Item - What a channel carries: a chunk of one side’s bytes, or the error that ended it.