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.
This commit is contained in:
@@ -299,6 +299,50 @@ client is caught when a write stalls past `write_timeout`, and graceful
|
||||
shutdown force-stops the connection at the drain deadline. See
|
||||
`examples/ws_echo.rs`.
|
||||
|
||||
The `WsHandler` trait also has a defaulted `on_open(&mut self, sender)`,
|
||||
called once before the frame loop — the place to subscribe or spawn
|
||||
producers for clients that may never send. Note the client sees the 101
|
||||
*before* `on_open` runs; a client that must know its subscriptions are
|
||||
live uses an application-level ack (any reply proves `on_open` completed,
|
||||
callbacks being sequential).
|
||||
|
||||
## Pub/Sub (v0.5)
|
||||
|
||||
`urus::pubsub` is a local-node, phoenix_pubsub-shaped topic broadcaster —
|
||||
independent of HTTP (it imports only smarm) and built for the WebSocket
|
||||
relay pattern:
|
||||
|
||||
```rust
|
||||
let bus: PubSub<String> = PubSub::new(); // in-runtime only!
|
||||
let rx = bus.subscribe("room:lobby")?; // Receiver<Arc<String>>
|
||||
bus.broadcast("room:lobby", "hi".to_string())?;
|
||||
bus.broadcast_from(smarm::self_pid(), "room:lobby", "no echo".into())?;
|
||||
```
|
||||
|
||||
One generic instance per message domain; payloads broadcast as `Arc<M>`
|
||||
(one allocation per broadcast). `subscribe` is idempotent per
|
||||
`(pid, topic)` and pins the subscription to the **calling actor** — its
|
||||
death prunes the entry via a monitor; a dropped `Receiver` is pruned
|
||||
lazily on the next broadcast. `subscribe_as(pid, topic)` pins to an
|
||||
explicit pid for relay patterns. Mailboxes are unbounded: `broadcast`
|
||||
never blocks the table, and a slow subscriber's memory bill is bounded
|
||||
by the two cleanup paths above.
|
||||
|
||||
Two composition rules that matter (both enforced by
|
||||
`shutdown_with_open_chat_terminates` in the integration suite):
|
||||
|
||||
1. `PubSub::new()` spawns an actor, so it must run in-runtime — build it
|
||||
lazily from a handler via a **non-static** `Arc<OnceLock<PubSub<M>>>`
|
||||
captured by the route closure. A `static` cell pins the table actor
|
||||
forever and graceful shutdown never returns.
|
||||
2. Relay/producer actors hold the `Receiver` (plus e.g. a `WsSender`
|
||||
clone) — **never a `PubSub` clone**, or relay and table keep each
|
||||
other alive past shutdown.
|
||||
|
||||
See [`examples/ws_chat.rs`](examples/ws_chat.rs): rooms as topics,
|
||||
`on_open` subscribes + spawns the relay, `on_message` uses
|
||||
`broadcast_from` so the speaker isn't echoed.
|
||||
|
||||
## Examples
|
||||
|
||||
### CRUD with Actor Ownership
|
||||
|
||||
Reference in New Issue
Block a user