docs: README streaming/SSE sections, ROADMAP v0.3 done, crud SSE ticker (v0.3 chunk 4)

README: write_timeout in the Config sample, read/write timeout-split
semantics spelled out, new Streaming Responses and Server-Sent Events
sections (producer contract, 1.0 fallback, heartbeat liveness), roadmap
summary bumped. ROADMAP: v0.3 marked done with the as-landed decisions
(pull shape per the user's fork answer; Q3 timeout split as proposed)
and verified exit criteria. crud example: GET /ticker SSE route — one
event/second, producer exits on SseClosed or the example's shutdown
flag (so an open ticker doesn't block AllDone past the drain).
This commit is contained in:
Claude
2026-06-12 04:59:39 +00:00
parent d14fb7c331
commit 43bd5a91a4
3 changed files with 126 additions and 18 deletions
+69 -5
View File
@@ -132,7 +132,8 @@ let cfg = Config {
listener_pool: 2, // Supervised accept-loop actors
scheduler_threads: Some(2), // smarm worker threads (None = one per CPU)
keep_alive_timeout: Duration::from_secs(60), // Idle budget between requests
request_timeout: Duration::from_secs(30), // Whole-request read deadline
request_timeout: Duration::from_secs(30), // Whole-request READ deadline
write_timeout: Duration::from_secs(30), // Per-write response budget
max_header_count: 64,
read_buf_size: 8 * 1024,
max_body_bytes: 1 << 20,
@@ -152,7 +153,14 @@ serve_with(cfg, pipeline).unwrap();
until its head and body are fully read. Expiry mid-head gets a
best-effort `408 Request Timeout`; expiry mid-body just closes. Deadlines
are absolute instants, so a trickling (slowloris-style) client can't
reset its budget by sending one byte at a time.
reset its budget by sending one byte at a time. Covers the **read phase
only** — a streaming response may legitimately outlive any whole-request
clock.
- `write_timeout` — a per-write budget for response bytes: the fixed
head+body write, and *each* streamed chunk, must complete within it. A
client that stops reading is dropped once its socket buffer fills and a
write stalls past the budget (the write-side twin of slowloris). Streamed
chunks each get a fresh budget; a stream as a whole has no deadline.
### Graceful Shutdown
@@ -185,6 +193,62 @@ If every `Handle` is dropped, shutdown can never be signalled and the
server runs forever — exactly `serve_with`'s semantics (it does this
internally).
### Streaming Responses
A handler can return a body it doesn't have yet: hand urus the read end of
a channel and feed it from a producer actor (the conn actor owns the
socket and all write deadlines; the producer never touches the fd):
```rust
use urus::{Conn, Next};
|c: Conn, _n: Next| {
let (tx, rx) = smarm::channel::<Vec<u8>>();
smarm::spawn(move || {
for part in ["chunk one", "chunk two"] {
if tx.send(part.as_bytes().to_vec()).is_err() {
return; // connection died — stop producing
}
}
// tx drops: end of stream.
});
c.put_status(200).put_body(rx)
}
```
On HTTP/1.1 the response uses `Transfer-Encoding: chunked` and the
connection is reusable afterwards; on HTTP/1.0 (no chunked framing) bytes
go out raw and the connection closes to delimit the body. End of stream is
signalled by dropping every `Sender`. The producer contract: a `send`
error means the connection is gone — exit. Chunked **request** bodies are
decoded transparently; handlers see `conn.body` either way.
### Server-Sent Events
`Conn::sse()` is the streaming body with the SSE headers and a heartbeat
wired up:
```rust
|c: Conn, _n: Next| {
let (c, events) = c.sse(); // or c.sse_with_heartbeat(interval)
smarm::spawn(move || {
loop {
if events.send("tick", "data").is_err() {
return; // SseClosed: client gone or server draining
}
smarm::sleep(std::time::Duration::from_secs(1));
}
});
c
}
```
While the producer is silent the connection actor emits `: keep-alive`
comment chunks every `HEARTBEAT_INTERVAL` (15s default). Liveness comes
from that ping: a vanished client is detected when a write stalls past
`write_timeout`. No request clock applies to an open SSE stream, and a
graceful shutdown force-stops it at the drain deadline.
### Named Actors
For introspection and debugging, the server registers itself in smarm's
@@ -262,9 +326,9 @@ This avoids the complexity of async ecosystems while maintaining full concurrenc
## Roadmap
See [`ROADMAP.md`](ROADMAP.md). Summary: v0.2 (supervised listener pool,
graceful shutdown, enforced timeouts, registry names) is done; next up are
streaming bodies + SSE (v0.3), WebSocket (v0.4), PubSub (v0.5), and
Phoenix-style channels (v0.6).
graceful shutdown, enforced timeouts, registry names) and v0.3 (streaming
bodies, chunked requests, SSE) are done; next up are WebSocket (v0.4),
PubSub (v0.5), and Phoenix-style channels (v0.6).
Refer to `urus-spec.md` and `urus-v1-build-notes.md` in the artifact persistence for the original design and implementation notes.