docs(roadmap): ratify v0.6 Channels design
User-ratified 2026-06-12. Resolves the v0.6 fork left by the v0.5 handoff: dep #4 is serde/serde_json, but ONLY behind an opt-in 'phoenix' feature (V2 codec + JsonCodec). The 'channels' core takes zero new deps — payload generic over urus Encode/Decode traits. Channel trait shape, ChannelSocket, TopicRouter/PrefixRouter, and opt-in ChannelSession persistence (zero-copy Arc<M> buffer drain) all specified in the v0.6 section.
This commit is contained in:
+62
-11
@@ -265,18 +265,69 @@ pid) + spawns the relay, `broadcast_from(self_pid(), ..)` for no-echo.
|
||||
|
||||
## v0.6 — Channels
|
||||
|
||||
Phoenix channels: the join/leave/event protocol over WebSocket transport,
|
||||
PubSub underneath.
|
||||
Join/leave/event protocol over WebSocket transport, PubSub underneath.
|
||||
Design decisions ratified pre-code (2026-06-12):
|
||||
|
||||
- `Channel` trait: `join(topic, payload, socket)`, `handle_in(event,
|
||||
payload, socket)`, `handle_out`, `terminate`.
|
||||
- Topic router (`"room:*"` patterns) → channel actor per (conn, topic),
|
||||
spawned under the ws connection, linked so conn death reaps channels.
|
||||
- Wire format: Phoenix V2 JSON serializer
|
||||
(`[join_ref, ref, topic, event, payload]`) — free interop with
|
||||
phoenix.js clients is the killer feature; needs a JSON dep or a
|
||||
hand-rolled mini-codec (decision point — this would be dep #3).
|
||||
- Presence: explicitly out of scope until distribution exists somewhere.
|
||||
**Feature flags.** Two additive features:
|
||||
- `"channels"` — the `Channel` trait, `TopicRouter` trait, `PrefixRouter`,
|
||||
`ChannelSocket`, session machinery. Zero new deps; pubsub is already
|
||||
always-on (it only exists because channels needs it).
|
||||
- `"phoenix"` — implies `"channels"` + `serde`/`serde_json` + the V2 frame
|
||||
codec. Opt-in phoenix.js interop; users who want protobuf or any other
|
||||
wire format take `"channels"` only and supply their own codec.
|
||||
|
||||
**Payload generics.** The `Channel` trait is generic over payload:
|
||||
`P: Encode + Decode` where `Encode`/`Decode` are urus codec traits (one
|
||||
method each). The `"phoenix"` feature provides a `JsonCodec` blanket impl
|
||||
via serde. No JSON anywhere in the `"channels"` surface.
|
||||
|
||||
**`Channel` trait shape.**
|
||||
```
|
||||
trait Channel<P>: Send + 'static {
|
||||
fn join(topic: &str, payload: P, socket: &ChannelSocket<P>) -> Result<P, P>;
|
||||
fn handle_in(&mut self, event: &str, payload: P, socket: &ChannelSocket<P>);
|
||||
fn terminate(&mut self) {}
|
||||
}
|
||||
```
|
||||
`handle_out` deferred — can land as a defaulted method, non-breaking.
|
||||
|
||||
**`ChannelSocket`.** Newtype over `WsSender` + a `PubSub` handle.
|
||||
Exposes `reply(ref, status, payload)`, `push(event, payload)`,
|
||||
`broadcast(topic, payload)`. The `ref` from the incoming frame is threaded
|
||||
in by the channel actor loop, invisible to the impl. Coupling to `PubSub`
|
||||
is intentional: channels owns pubsub, and broadcast is the core use case.
|
||||
|
||||
**Topic router.** `TopicRouter` trait: `fn route(&self, topic: &str) ->
|
||||
Option<Box<dyn ChannelFactory<P>>>`. Factory receives the full raw topic
|
||||
string so wildcard-segment extraction is always possible. urus ships
|
||||
`PrefixRouter` as the default impl: scan to `'*'`, discard the rest,
|
||||
single `HashMap` lookup on the prefix. No regex dep, no glob machinery —
|
||||
users who need that implement `TopicRouter` themselves.
|
||||
|
||||
**Channel actor lifetime + session persistence.**
|
||||
Default: channel actor linked to the ws connection actor, cold-start on
|
||||
every reconnect (phoenix-server-compatible behavior). Opt-in persistence
|
||||
via a `ChannelSession` trait:
|
||||
```
|
||||
trait ChannelSession: Send + 'static {
|
||||
type Key: Eq + Hash + Send + 'static;
|
||||
fn session_key(topic: &str, join_payload: &P) -> Self::Key;
|
||||
fn buffer_cap() -> usize { 128 }
|
||||
fn ttl() -> Duration { Duration::from_secs(30) }
|
||||
}
|
||||
```
|
||||
When implemented, the router consults a session registry (gen_server,
|
||||
same pattern as the connection registry) before spawning: an existing
|
||||
actor is reattached, and buffered outbound messages (`Vec<Arc<M>>`, no
|
||||
re-serialisation) are drained to the reconnecting transport. Buffer full
|
||||
or TTL expired → actor tears down; next join is a cold start. This
|
||||
diverges from Phoenix server conventions (client-re-syncs) but is
|
||||
invisible to phoenix.js at the wire level. Zero-copy drain is the smarm
|
||||
motivation: messages are `Arc<M>` in the buffer and in the PubSub relay
|
||||
path, one allocation per broadcast regardless of subscriber count or
|
||||
reconnect cycles.
|
||||
|
||||
**Presence:** explicitly out of scope until distribution exists.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user