//! Phoenix-style channels (v0.6): join/leave/event protocol over the //! v0.4 WebSocket duplex, pubsub underneath, phoenix V2 JSON on the //! wire. //! //! cargo run --example channels_chat --features phoenix //! websocat ws://127.0.0.1:8080/socket (run two of these) //! ["1","1","room:lobby","phx_join",{"name":"alice"}] //! ["1","2","room:lobby","shout",{"text":"hi"}] //! ["1","3","room:lobby","phx_leave",{}] //! //! The shapes this demonstrates: //! //! - **One `ChannelHub` for the whole app**, built lazily on the first //! connection via the NON-static `Arc>` pattern (the //! hub spawns the pubsub table; same in-runtime + shutdown laws as //! `examples/ws_chat.rs`, see the docs there). //! //! - **A channel per joined topic, not per socket.** `Room` never sees //! frames, refs, or the transport heartbeat — the conn-side handler //! answers heartbeats, routes `phx_join`, and the per-join channel //! actor (linked to the connection) runs the callbacks sequentially. //! //! - **`Json` is the phoenix codec's opt-in newtype.** The //! `"channels"` core is wire-neutral (`Encode`/`Decode`, zero deps); //! `Json` here buys phoenix.js V2 interop. Typed payloads work //! the same way: `Json` for any serde type. //! //! - **`session::*` topics persist across reconnects** (opt-in via //! `channel_session`): drop a websocat mid-room, reconnect and rejoin //! within 30s, and the broadcasts you missed drain to you in order — //! the buffer holds the relay path's own `Arc`s, nothing is copied. //! The session is keyed by the `"name"` in the join payload. use std::sync::{Arc, OnceLock}; use serde_json::{json, Value}; use urus::channels::phoenix::Json; use urus::{ serve_with_shutdown, shutdown_handle, Channel, ChannelHub, ChannelSession, ChannelSocket, Config, Conn, Next, Pipeline, PrefixRouter, Router, Status, }; type P = Json; /// One chat room. State lives across callbacks (and, for `session:*` /// topics, across transports). #[derive(Default)] struct Room { name: String, msgs_seen: u64, } impl Channel

for Room { fn join(&mut self, topic: &str, payload: P, _s: &ChannelSocket

) -> Result { // Re-entrant by contract for session topics: a rejoin lands // here again on the same instance — re-auth, then ack. self.name = payload.0["name"].as_str().unwrap_or("anon").to_string(); Ok(Json(json!({ "joined": topic, "as": self.name, "seen": self.msgs_seen }))) } fn handle_in(&mut self, event: &str, payload: P, s: &ChannelSocket

) { match event { "shout" => { self.msgs_seen += 1; s.broadcast("shouted", Json(json!({ "from": self.name, "msg": payload.0 }))); } _ => s.reply(Status::Ok, Json(json!({ "echo": event }))), } } fn terminate(&mut self) { println!("room channel for {:?} is over", self.name); } } /// Session config for `session:*`: the join payload's `"name"` is the /// session token. Defaults: 128 buffered broadcasts, 30s TTL. struct BySessionName; impl ChannelSession

for BySessionName { type Key = String; fn session_key(topic: &str, join_payload: &P) -> String { format!("{topic}/{}", join_payload.0["name"].as_str().unwrap_or("anon")) } } fn main() { let hub: Arc>> = Arc::new(OnceLock::new()); let router = Router::new().get("/socket", move |c: Conn, _n: Next| { // Hub construction is in-runtime only (it spawns the pubsub // table and, for session patterns, their registries); the // non-static cell is what lets shutdown drain them all. let hub = hub.get_or_init(|| { ChannelHub::new( PrefixRouter::new() .channel_default::("room:*") .channel_session::("session:*", |_: &str| { Box::new(Room::default()) as Box> }), ) }); hub.upgrade(c) }); let (handle, signal) = shutdown_handle(); std::thread::spawn(move || { println!("channels_chat on ws://127.0.0.1:8080/socket — press Enter to shut down"); let mut line = String::new(); let _ = std::io::stdin().read_line(&mut line); handle.shutdown(); }); serve_with_shutdown(Config::new("0.0.0.0:8080".parse().unwrap()), Pipeline::new().plug(router), signal) .unwrap(); println!("channels_chat: drained, bye"); }