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.
This commit is contained in:
Claude
2026-06-12 15:26:21 +00:00
parent 0de2baa72d
commit 86c8d31b93
4 changed files with 225 additions and 4 deletions
+84 -3
View File
@@ -343,6 +343,87 @@ 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.
## Channels (v0.6)
Phoenix-style join/leave/event multiplexing over the WebSocket duplex,
pubsub underneath. Two additive features:
- `channels` — the wire-neutral core (`Channel`, `ChannelHub`,
`PrefixRouter`, `ChannelSocket`, sessions). Zero new dependencies;
bring your own codec by implementing `Encode`/`Decode` for your
payload type.
- `phoenix` — implies `channels`, adds serde/serde_json and the
phoenix.js V2 wire format via the `Json<T>` newtype.
```rust
use urus::{Channel, ChannelHub, ChannelSocket, PrefixRouter, Status};
use urus::channels::phoenix::Json;
use serde_json::{json, Value};
type P = Json<Value>;
#[derive(Default)]
struct Room;
impl Channel<P> for Room {
fn join(&mut self, topic: &str, payload: P, _s: &ChannelSocket<P>) -> Result<P, P> {
Ok(Json(json!({ "joined": topic }))) // Err(..) rejects
}
fn handle_in(&mut self, event: &str, payload: P, s: &ChannelSocket<P>) {
match event {
"shout" => s.broadcast("shouted", payload),
_ => s.reply(Status::Ok, Json(json!({ "echo": event }))),
}
}
}
// in-runtime, non-static — the ws_chat OnceLock pattern applies
let hub = ChannelHub::new(PrefixRouter::new().channel_default::<Room>("room:*"));
// route handler: hub.upgrade(conn)
// from anywhere with a hub handle: hub.broadcast("room:lobby", "news", payload)
```
One channel actor per joined topic per socket, linked to the
connection: callbacks are sequential, a channel panic is a socket
event, and the conn-side handler answers transport heartbeats and
routes joins — `Channel` impls never see frames or refs (`reply`
correlates to the in-flight event invisibly).
**Session persistence (opt-in).** By default channels cold-start per
join, Phoenix-server style. Implementing `ChannelSession` and
registering with `channel_session` makes the channel actor outlive its
transport: broadcasts that arrive while detached are buffered (the same
`Arc`s the relay path carries — zero re-serialisation) and drained, in
order, under the rejoin's new generation. Buffer cap or TTL exceeded,
explicit leave, or a rejected rejoin tears the session down; the next
join is a cold start. A second transport claiming the same key evicts
the first.
```rust
struct ByToken;
impl urus::ChannelSession<P> for ByToken {
type Key = String;
fn session_key(topic: &str, join_payload: &P) -> String {
format!("{topic}/{}", join_payload.0["token"].as_str().unwrap_or(""))
}
// fn buffer_cap() -> usize { 128 } // defaults
// fn ttl() -> Duration { Duration::from_secs(30) }
}
PrefixRouter::new().channel_session::<ByToken>("session:*", |_: &str| {
Box::new(Room::default()) as Box<dyn Channel<P>>
})
```
Session channels must treat `join` as re-entrant: every reattach calls
it again on the same instance (the rejoin ack needs a payload, and the
channel gets to re-auth).
See [`examples/channels_chat.rs`](examples/channels_chat.rs)
(`cargo run --example channels_chat --features phoenix`): ephemeral
`room:*` and persistent `session:*` side by side over phoenix.js V2
JSON.
## Examples
### CRUD with Actor Ownership
@@ -414,9 +495,9 @@ This avoids the complexity of async ecosystems while maintaining full concurrenc
See [`ROADMAP.md`](ROADMAP.md). Summary: v0.2 (supervised listener pool,
graceful shutdown, enforced timeouts, registry names), v0.3 (streaming
bodies, chunked requests, SSE) and v0.4 (WebSocket: handshake, frame
codec, duplex handler API) are done; next up are PubSub (v0.5) and
Phoenix-style channels (v0.6).
bodies, chunked requests, SSE), v0.4 (WebSocket: handshake, frame
codec, duplex handler API), v0.5 (PubSub) and v0.6 (Phoenix-style
channels with opt-in session persistence) are done.
Refer to `urus-spec.md` and `urus-v1-build-notes.md` in the artifact persistence for the original design and implementation notes.