docs: record the supervised-bus cycle (v0.8) and retire the OnceLock idiom

ROADMAP gains a v0.8 entry: the smarm 415effb lifetime change as root cause,
the description/instantiation split, the tree shape, the accepted per-call
resolution cost, and what stays open (RegistryName newtype, PubSub's
unprotectable const, dynamic session actors, hammer.sh's missing feature
matrix). The v0.7 "unreproduced test failure" open item is closed out and
pointed at it — it was this hang, hidden because hammer.sh builds with default
features while the failure needs --all-features load.

README and the two module headers still taught the v0.5 rules: in-runtime-only
construction, the non-static Arc<OnceLock<..>> cell, and "a relay must never
hold a PubSub clone". None of those are true any more; they are replaced by
what actually holds now, with a note on what changed for anyone who learnt the
old shape.

session.rs also gains an honest note that its session actors are the last
lifetime in urus implied by a drop rather than stated — they are dynamic, so a
fixed ChildSpec list cannot hold them, and the trigger is at least a command
now rather than a refcount.
This commit is contained in:
Claude
2026-08-20 15:01:44 +00:00
parent 9eaa85b9df
commit e02527a606
4 changed files with 126 additions and 33 deletions
+26 -17
View File
@@ -358,10 +358,11 @@ 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())?;
const BUS: PubSub<String> = PubSub::new("chat"); // a name; spawns nothing
// BUS.child() goes in your supervision tree (or serve_with*'s child vec)
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>`
@@ -373,19 +374,25 @@ 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):
Composition (enforced by `shutdown_with_open_chat_terminates` in the
integration suite):
1. `PubSub::new()` spawns an actor, so it must run in-runtime. If your
app owns its tree, start the table as a supervised sibling of the
endpoint and address it by name — the clean shape. Under `serve*`
there is no in-runtime moment before the first request, so 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.
1. **The handle is an address, not the table.** `PubSub<M>` is a name:
`const`, `Copy`, spawns nothing, fine in a `static` or outside the
runtime. The actor is `PubSub::child()`, a `ChildSpec` for your
supervision tree — or for `serve_with*`'s app-children vec, which puts
it ahead of the endpoint so it stops only after the endpoint has
drained. Operations resolve the name per call, so a restarted table is
reached transparently.
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.
clone). They may hold the handle too — it pins nothing — but usually
have no use for one.
Prior to v0.8 both of these read the other way round: `PubSub::new()`
spawned the table, the handle owned its life, and a non-static
`Arc<OnceLock<PubSub<M>>>` lazily built from the first handler was the
required idiom. smarm 0.7 made a server's lifetime its own, and that
whole apparatus went away with it.
See [`examples/ws_chat.rs`](examples/ws_chat.rs): rooms as topics,
`on_open` subscribes + spawns the relay, `on_message` uses
@@ -425,8 +432,10 @@ impl Channel<P> for Room {
}
}
// in-runtime, non-static — the ws_chat OnceLock pattern applies
let hub = ChannelHub::new(PrefixRouter::new().channel_default::<Room>("room:*"));
// A description: spawns nothing. `children` are the ChildSpecs it needs
// (bus table + one registry per session route) — hand them to serve_with*.
let (hub, children) =
ChannelHub::new("chat-bus", PrefixRouter::new().channel_default::<Room>("room:*"));
// route handler: hub.upgrade(conn)
// from anywhere with a hub handle: hub.broadcast("room:lobby", "news", payload)
```