6 Commits
Author SHA1 Message Date
Claude 86c8d31b93 docs(channels): example, README section, ROADMAP v0.6 mark-done — chunk 4
examples/channels_chat.rs (required-features phoenix): ephemeral
room:* and persistent session:* side by side over phoenix.js V2 JSON,
stdin-driven graceful shutdown (the ws_chat pattern). Smoke-tested
live: rejoin after transport drop lands on the same instance (state
counter persists across three transports), broadcasts carry the new
join generation's ref.

README: channels section (feature split, core trait walkthrough,
session persistence semantics incl. the re-entrant-join contract).
ROADMAP: v0.6 marked done with the full as-landed decision list,
chunked as committed.
2026-06-12 15:26:21 +00:00
Claude ad19848db3 feat(ws+pubsub): on_open hook + ws_chat — v0.5 chunk 2
WsHandler::on_open(&mut self, sender) — defaulted, non-breaking,
added mid-chunk for veto-by-diff (the v0.3 "push" precedent for
defaults-level decisions): without it a listen-only ws client can
never be subscribed, since on_message never fires for a client that
doesn't send. Runs exactly once, first, in the conn actor, before any
buffered pipelined frame is decoded. Panic contract identical to
on_message: check_cancelled re-raise dance, else 1011 and no
on_close.

DISCOVERY 1 (documented, not fixed — inherent): the 101 reaches the
client BEFORE on_open runs, so "is my subscription live yet" is a
race client-side. App-level ack is the answer (any reply proves
on_open completed; callbacks are sequential). The integration tests
hit this immediately (first read of a join notice flaked into a 5s
read-timeout) and use a sync/synced ack; same reason Phoenix joins
reply.

DISCOVERY 2 (the structural one): PubSub::new() is in-runtime only,
but pipelines are built pre-runtime. crud's static-OnceLock bootstrap
DOES NOT COMPOSE with serve_with_shutdown here: a static pins the
table actor's last ServerRef forever, its inbox never closes, the
gen_server never exits, AllDone is unreachable — serve hangs (the
cross-thread-unpark limitation closes the workaround routes). The
pattern that works: NON-static Arc<OnceLock<PubSub<M>>> captured by
the route closure. Drain stops conns+listeners -> last Arc<Pipeline>
drops IN-runtime -> cell+handle drop -> inbox closes -> table exits.
Corollary: relays hold the Receiver only, NEVER a PubSub clone
(relay holds table's inbox open, table holds relay's receiver open:
mutual keepalive, shutdown hangs). Both rules in module docs, README,
and enforced by the new shutdown_with_open_chat_terminates test:
two open chat sockets + live relays + live table, shutdown must
return within 5s.

examples/ws_chat.rs: rooms as topics (room:{name}), on_open
subscribes (conn-actor pid: the monitor scopes cleanup to the
session) + spawns the relay (rx -> WsSender clone; exits on prune or
WsClosed), on_message broadcast_from(self_pid()) so the speaker is
never echoed, on_close broadcasts the leave notice and relies on the
monitor for unsubscribe.

Integration: ws_chat_broadcast_reaches_other_client_not_sender
(no-echo proven orderingly, no timeout reads: B speaks after A, A's
next frame must be B's) + shutdown_with_open_chat_terminates.

hammer.sh default subset now includes ws_ (v0.4 ran it ad hoc; ws IS
conn-lifecycle). Hammered: 35x subset (incl. both new tests) + 3x
full + 1x full under smarm-trace, all green. dbg!-grep clean. smarm
PRISTINE. README pub/sub + on_open sections; ROADMAP v0.5 as-landed.

Suite: 67 unit + 42 integration + 2 doc.
2026-06-12 10:30:08 +00:00
Claude ffc579c500 feat(ws): duplex wiring + handler API + echo example (v0.4 chunk 3)
Topology (b) as agreed: ONE actor per connection via smarm RFC 008 fd
arms. After the 101, ws::duplex::run_duplex replaces the HTTP loop:
try_select(&[&outbound_rx, &FdArm::readable(fd)]) per iteration,
outbound at index 0 (priority order = owed writes drain before reads;
honest backpressure). Accepted gap documented: mid-write of a large
outbound frame the actor isn't reading.

Handler API (deferred questions, as answered this session):
- WsHandler { on_message, on_close } runs IN the conn actor's select
  loop; concurrency = spawn an actor with a WsSender clone (the SSE
  producer pattern). Conn::upgrade(self, handler) flat signature;
  WsUpgrade grows from marker to Box<dyn WsHandler> payload.
- WsSender: Clone; send/text/binary/ping/close -> Err(WsClosed).
  Unbounded channel like SSE; slow client bounded by write_timeout.
- Control frames invisible v1: auto-pong inline (5.5.2), peer close
  auto-echoed (5.5.1; server closes TCP first per 7.1.1) surfacing
  only as on_close(Some(code), reason); wordless endings (EOF, write
  failure, drain timeout) = on_close(None, ""). on_ping/on_pong can
  land later as default methods, non-breaking.
- Server-initiated close (WsSender::close, FrameError->1002/1009/1007,
  handler panic->1011): close out, bounded drain (one write_timeout
  budget) for the peer echo, data discarded (1.4). Handler panics
  re-raise smarm's stop sentinel first (the pipeline catch_unwind
  dance, replicated).
- Caps: Config.max_frame_payload (1 MiB) / max_message_bytes (4 MiB).

Conn actor: 101 branch now hands fd + leftover buf (pipelined first
frame carries over; tested) + boxed handler to run_duplex; registry
entry stays Busy for the ws lifetime, so graceful shutdown force-stops
the conn out of the select park at the drain deadline (tested — the
in-runtime request_stop wake covers select parks too).

Tests: chunk-1 EOF test rewritten into a 9-test duplex suite (echo,
pipelined first frame, ping/pong, both close directions, 1002 unmasked,
1009 header-cap, fragmentation with interleaved ping, shutdown
force-stop). Suite 59u+40i+2d. Hammer: 35x lifecycle+ws subset + 3 full
+ 1 full under smarm-trace, all green; no AlreadyExists out of
try_select (the fresh eager-cleanup path held). Validated against
python websocket-client (echo, pong payload, close 1000).
examples/ws_echo.rs: /echo in-actor + /clock producer-spawning.
2026-06-12 09:44:35 +00:00
Claude 43bd5a91a4 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).
2026-06-12 04:59:39 +00:00
Claude 8065ea561e feat(serve): registry names + docs (v0.2 chunk 4)
Registration:
- Each listener registers "urus.listener.{i}" from inside its ChildSpec
  factory. On a restart the stale binding points at a dead pid; smarm's
  registry evicts those lazily on register/whereis, so re-registering
  the same name is safe. Result ignored — a registry hiccup must not
  take the listener down.
- "urus.server" -> supervisor pid, registered via the JoinHandle's
  .pid() immediately after spawn rather than inside the closure: the
  binding exists before the supervisor runs an instruction, so an early
  whereis can't observe None.

Test: server_and_listeners_are_registered probes whereis from a route
handler — whereis must run inside the runtime, and the test thread is a
foreign OS thread with no runtime in its TLS.

Docs:
- README: Config documented in full (timeout semantics: keep_alive =
  idle-before-first-byte, request = Instant deadline first-byte ->
  head+body, slowloris-proof), Handle/shutdown_handle/serve_with_shutdown
  section with the 4-step shutdown sequence, named-actors note, test
  coverage list refreshed, Roadmap section now points at ROADMAP.md.
- ROADMAP: v0.2 chunks 1-4 marked landed; design detail stays in the
  chunk commit bodies.

Verified: cargo build --features smarm-trace clean; full suite (32
tests) green 3x; debug-grep clean.

This closes v0.2. Exit criteria all verified across chunks 1-3: crud
drains on stdin-Enter (~100ms), idle keep-alive reaped at
keep_alive_timeout, panicking listener restarts under load.
2026-06-11 22:24:02 +00:00
Markk116 a644fec6f5 doc: add readme 2026-05-26 23:23:11 +02:00