StreamConn
struct StreamConnFramework-owned per-connection handle.
Owns the accepted client stream and a per-connection cancel cell,
carries a stable per-connection id (for the handler to key its own
typed per-connection state), and the close flag the handler trips
when it is done with the connection. The client stream is owned
here (sole owner) and closed when the handle is dropped -- the same
ownership rule the reactor's ConnHandle follows.
For the common streaming-proxy shape it also owns one optional
UpstreamChunkSource: attach_upstream(source) moves the source
in, relay_upstream drains it, and teardown closes it -- so a
relay front carries no per-connection table and never names a file
descriptor or a byte Span.
Implements Movable
- Fields
client: TcpStream: The accepted client connection. Sole owner; closed on drop.out_buf: List[UInt8]: Pending outbound bytes.sendappends here; the blocking driver flushes withflush_blockingand the reactor drains incrementally withdrain_nonblockingon writable edges. This is the single owned write buffer (no per-slotalloctable).out_pos: Int: Bytes ofout_bufalready written to the socket. The unwritten tail isout_buf[out_pos:];out_pos == len(out_buf)means fully drained.
__init__
def __init__(out self, var client: TcpStream, id: Int = Int(0)) Adopt client into a fresh per-connection handle.
id
def id(self) -> Int Stable per-connection id (key for handler-owned per-conn state).
fd
def fd(self) -> c_int Underlying client fd. Fast accessor; does not check state.
cancel
def cancel(mut self) -> Cancel Return a Cancel handle bound to this connection's cell.
flip_cancel
def flip_cancel(mut self, reason: Int) Flip the connection's cancel cell (framework-internal).
request_close
def request_close(mut self) Ask the framework to close this connection after the current lifecycle step returns.
is_closing
def is_closing(self) -> Bool True once request_close has been called.
attach_upstream
def attach_upstream(mut self, var src: UpstreamChunkSource) Attach an upstream source and let the framework run it.
This is the recommended, low-ceremony path: hand the connection
the source and the framework takes over -- it reads the fd to
watch internally (no descriptors in front code), owns the
source for the connection's lifetime (closing it on teardown),
and relay_upstream drains it. The front never keeps a
per-connection table, never names a file descriptor, and never
remembers to close anything.
def on_open(mut self, mut conn: StreamConn) raises:
conn.attach_upstream(UpstreamChunkSource.connect(self.backend))
def attach_upstream(mut self, fd: Int) Low-level: ask the reactor to watch a raw fd (a front-owned upstream socket / pipe / eventfd) for readability.
Prefer the attach_upstream(UpstreamChunkSource) overload --
it hides the descriptor and the lifetime. Use this only for an
upstream that is not an UpstreamChunkSource (a bare pipe, an
eventfd). The front then owns fd's lifetime: it opened the
upstream and must close it (typically by holding the stream in
its own per-connection state and dropping it in on_close).
The framework only adds/removes fd from the event loop.
detach_upstream
def detach_upstream(mut self) Stop watching the attached upstream and drop any framework-owned source. The reactor unregisters the fd on the next reconcile.
has_upstream
def has_upstream(self) -> Bool True if an upstream fd is currently attached.
has_attached_source
def has_attached_source(self) -> Bool True if a framework-owned UpstreamChunkSource is attached (via the source-taking attach_upstream).
upstream
def upstream(mut self) -> ref[self._upstream._value] UpstreamChunkSource The framework-owned upstream source, for fronts that want to poll it directly instead of using relay_upstream.
Only valid when has_attached_source(); call after attaching
via attach_upstream(UpstreamChunkSource).
relay_upstream
def relay_upstream(mut self) Drain the attached source into the client with backpressure.
The standard on_upstream body, shipped once so every relay
front does not re-derive it: pull ready chunks and send them
until the client write buffer hits the high watermark (so a
single readable edge cannot overshoot the bound -- the reactor
re-arms upstream interest when the client drains), request close
on EOF, and stop on pending (the reactor parks on the fd, no
spin). No-op if no source is attached.
def on_upstream(mut self, mut conn: StreamConn) raises:
conn.relay_upstream()
upstream_fd
def upstream_fd(self) -> Int The attached upstream fd, or -1 if none.
reg_upstream_fd
def reg_upstream_fd(self) -> Int Framework-internal: the upstream fd currently registered with the reactor, or -1.
set_watermarks
def set_watermarks(mut self, hi: Int, lo: Int) Tune the relay-buffer hi/lo watermarks for this connection.
hi is the occupancy at which the reactor stops reading the
attached upstream; lo (< hi) is where it resumes. Values
are clamped to a sane order (lo forced below hi) so a
front cannot wedge the gate shut. Call in on_open to size the
relay pipe before any bytes flow.
hi_watermark
def hi_watermark(self) -> Int The high watermark in bytes.
lo_watermark
def lo_watermark(self) -> Int The low watermark in bytes.
write_buffer_full
def write_buffer_full(self) -> Bool True once unwritten outbound bytes reach the high watermark.
A relay front should check this in its on_upstream drain loop
and stop pulling from the upstream when it returns True, so a
single readable edge cannot overshoot the bound before the
reactor's interest gate takes effect on the next reconcile.
upstream_paused
def upstream_paused(self) -> Bool True while upstream reads are gated off by backpressure.
pause_count
def pause_count(self) -> Int How many times upstream reads were paused (hi crossings).
resume_count
def resume_count(self) -> Int How many times upstream reads were resumed (lo crossings).
apply_backpressure
def apply_backpressure(mut self) -> Bool Recompute the upstream read gate from buffer occupancy with hi/lo hysteresis; update the paused flag and crossing counters.
Returns whether the attached upstream fd should currently be read
(True = arm INTEREST_READ; False = pause). Pauses on
crossing the high watermark, resumes on falling to the low one;
between the two it holds the prior state -- the band that stops
interest thrash. Framework-internal: the reactor calls this on
each reconcile.
reg_upstream_interest
def reg_upstream_interest(self) -> Int Framework-internal: upstream interest bits currently registered (-1 unknown).
set_nonblocking
def set_nonblocking(mut self, enabled: Bool) Toggle non-blocking mode on the client socket (reactor path sets this once at accept).
send
def send(mut self, data: Span[UInt8]) Queue data for delivery to the client.
Buffered, not written immediately: the blocking driver flushes
with flush_blocking after each lifecycle step; the reactor
drains with drain_nonblocking on writable edges. Same call
site in both modes, so a front never blocks the event loop.
Token-burst coalescing: each ``send`` is an O(n) memcpy into
the single contiguous ``out_buf`` -- it issues no syscall. When a
front emits K ready chunks in one reactor tick (call ``send`` K
times), the subsequent drain flushes all K in ONE ``send(2)``,
removing the per-token syscall round-trip that dominates long-
output nTPOT. The gather is a memcpy into out_buf,
not a zero-copy writev over the K chunk buffers; swap in
writev_buf over a chunk vector only if that memcpy shows up in
a profile -- the syscall count (the nTPOT win) is already 1.
def send(mut self, data: List[UInt8]) Queue an owned byte list for the client (no Span wrap).
Convenience over send(Span[UInt8, _]) for the common case
where a front holds a List[UInt8] -- e.g. a chunk just taken
from a ChunkPoll. Same coalescing semantics.
def send(mut self, text: StringSpan) Queue a UTF-8 string for the client (no byte juggling).
Convenience for text protocols (SSE, NDJSON, a status line):
conn.send("data: " + line + "\n"). Appends the string's
UTF-8 bytes with the same coalescing as send(Span[UInt8, _]).
send_response
def send_response(mut self, resp: ResponseImpl, keep_alive: Bool = False) Queue a Handler-produced Response as this stream's response head (and fixed body, if any).
This is the shared-middleware bridge between serve and
serve_streaming: build one Handler middleware stack
(e.g. Logger[RequestId[Router]]), call stack.serve(req)
inside on_open, then conn.send_response(resp) -- the
streaming front gets the exact logging / request-id / routing
behaviour the request/response path has, and keeps
conn.send(...) for the streamed tail (SSE tokens, relayed
chunks). Without this a streaming front had to hand-roll its
status line and headers as a raw string, bypassing the
middleware stack entirely.
Content-Length is emitted from the body length unless the
response already declares Transfer-Encoding or
Content-Length (chunked / SSE fronts set Transfer-Encoding
and stream the body via send). Connection is always set
from keep_alive (any inbound Connection header is
dropped). This is HTTP/1.x framing only -- a streaming front
speaks h1 to the client; h2/h3 response heads have their own
framing on those paths.
The head framing is the shared, transport-agnostic
frame_h1_stream_head_into adapter (see
flare/http/_server/write.mojo), so an HTTPS front streaming
over TlsConnHandle frames its head identically -- the bytes
just ride SSL_write instead of send.
pending_out
def pending_out(self) -> Int Unwritten outbound bytes (len(out_buf) - out_pos).
has_pending_out
def has_pending_out(self) -> Bool True if there are unwritten outbound bytes.
write_syscalls
def write_syscalls(self) -> Int Number of send(2) calls issued so far by the drain path. A burst of K chunks queued via send flushes in one syscall, so this advances by ~1 per drain regardless of K.
reset_write_syscalls
def reset_write_syscalls(mut self) Reset the write-syscall counter (for microbenches).
flush_blocking
def flush_blocking(mut self) Write all pending outbound bytes, blocking. Used by the single-connection driver.
drain_nonblocking
def drain_nonblocking(mut self) -> Bool Write as much of the pending buffer as the socket accepts without blocking.
Returns True once the buffer is fully flushed, False if
the socket would block (EAGAIN) with bytes still pending -- the
caller then keeps INTEREST_WRITE armed and retries on the
next writable edge. Raises on a real socket error (EPIPE /
ECONNRESET) so the reactor closes the connection.
recv
def recv(mut self, mut buf: List[UInt8], max_bytes: Int) -> Int Append up to max_bytes bytes read from the client onto buf. Returns the count read (0 on peer EOF).
enable_inbound
def enable_inbound(mut self, enabled: Bool = True) Opt into front-owned inbound body consumption.
Call in on_open for a front that reads a (possibly large)
request body itself via read_body. While enabled the reactor
no longer drains-and-discards client bytes for FIN detection, so
the body bytes reach the front intact; the front pulls them in
bounded chunks (peak memory independent of body size) and detects
end-of-body when read_body returns eof.
inbound_enabled
def inbound_enabled(self) -> Bool True if the front owns inbound body consumption.
read_body
def read_body(mut self, max_bytes: Int = Int(65536)) -> ChunkPoll Read up to max_bytes of the inbound request body, without blocking.
The result is a ChunkPoll (so inbound and outbound share
one tri-state shape):
ready(bytes)-- a body chunk is available now;eof()-- the client finished the body (clean FIN);pending(fd)-- nothing buffered yet; the reactor will fire again on the next readable edge (no busy-poll).
A front loops on this in on_writable (the streaming reactor
keeps the connection live), processing each chunk and dropping it,
so peak memory is one chunk regardless of total body size up to
whatever ceiling the front enforces.