phoenixframework/phoenix — pull request #570 by chrismccord (2014-12-31 18:50:27+00:00)
Title: Refactor Channel Layer

I believe I have addressed everything discussed in #478. @josevalim please take a thorough look once you have a chance because it ended up being less rework than I thought. The Publisher (PubSub) API remained exactly the same and is implemented per your description. Notable changes:

1. `socket` API in the router. Socket endpoints are now configured via the `socket` macro and house the channel definitions. Channels can also now be scoped based on transport, i.e.:

```elixir
defmodule MyApp.Router do
use Phoenix.Router

socket "/ws", MyApp do
channel "topic1:*", MyChannel
channel "baretopic", MyChannel
channel "wsonly:*", MyChannel, via: [Phoenix.Transports.WebSocket]
channel "lponly:*", MyChannel, via: [Phoenix.Transports.LongPoller]
end
...

```

2. The "topic" abstraction is now *only a string identifier*. We used to conflate the channel name and topic names. Now `channel "foo:*"` refers to the topic "foo" and any pattern following "foo:". By convention, we use "topic:subtopic", but the splat "*" does not require a colon.

3. Likewise, in the channel callbacks, you now match on the topic directly:

```elixir
# router
channel "rooms:*", RoomChannel

# channel
def join("rooms:lobby", message, socket) do ...
def join("rooms:" <> room_id, message, socket) do ...
```

4. All channel callbacks now accept `socket` as the last argument to more closely follow a GenServer API where the state is held in the last arg and the message is matched int he first.

5. Transports now use two flavors of messages, `{:socket_reply, %Message{...}}` and `{:socket_broadcast, %Message{...})` to differentiate replies from broadcasts. This was required to support `outgoing` callbacks.

6. `event` has been renamed to `incoming` and now channel callbacks can filter/intercept/modify broadcasts with an `outgoing` callback for per-socket customization, i.e.:

```elixir
def incoming("new:msg", %{from: from_id, "body" => body}, socket) do
# broadcast incoming new message to all subscribers
broadcast socket, "new:msg", %{body: body, from_id: from_id}
end

# each socket's outgoing callback will be triggered for per-socket
# customization of broadcast. Sockets can also not reply to filter broadcasts
# from being relayed.
def outgoing("new:msg", msg, socket) do
user = socket.assigns[:user]

reply socket, "new:msg", Dict.merge(msg,
is_editable: can_edit?(user, msg.from_id),
is_friend: is_friend?(user, msg.from_id)
)
end

# by default just forward reply
def outgoing(event, msg, socket) do
reply socket, event, msg
end
```

### Points of discussion for the 0.8.0 release:

- Should incoming/outgoing be renamed to a more GenServer'esque API? i.e. `handle_incoming`, `handle_outgoing`? I prefer the current naming, but `handle_` could be more welcoming to newcomers who would find it more familiar to GenServer style.
- Should we embed the `node()` my default in the message to allow per-node filtering? I opted not to since end-users can attach their `node()` themselves in the broadcasted message if they need per-node handling. Subscribers can pick up the message the compare their node().
- Others?

### Points of discussion for 0.9.0+:

As discussed on IRC, we need to tackle handling missed messages with a configurable backend. Today, the LongPoller requires a cookie session, and clustered nodes. The PubSub also requires clustering. We would like to move towards a configurable store with `last_seen_id` tracking of messages, for replaying messages between client drops, as well as a solution to non-clustered pubsub servers.

Let me know how it looks! Thanks