I have an Phoenix app that is deliberately over-using channels, to see how far I can push it and shake out the gotchas.
Last week I upgraded from 0.13.x to 0.15.0 (through 0.14.x) and hit problems with models that I'd been sending over channels to the Javascript client.
def handle_in("project_email_recipients", _, socket) do
# Retrieves all the models for the project
recipients = ProjectEmailing.project_recipients(
socket.assigns[:project_id])
{:reply,
{:ok, %{project_email_recipients: recipients}},
socket}
end
This was serialising just fine and being received as maniputable JSON at the client. On upgrade it broke. On investigation I discovered that Ecto upgraded from 0.13.1 to 0.14.3. Under 0.13.1, a model's (Project model) __meta__
value looks something like this:
%Ecto.Schema.Metadata{source: "projects", state: :loaded}
Under 0.14.3 it looks like:
%Ecto.Schema.Metadata{source: {nil, "projects"}, state: :loaded}
The Poision encoder doesn't cope with the tuple, and leads to this error:
** (Poison.EncodeError) unable to encode value: {nil, "projects"}
(poison) lib/poison/encoder.ex:213: Poison.Encoder.Any.encode/2
On reflection, serialising everything in the model record to JSON, including the __meta__
field was silly; I should be more picky. There's two sensible ways to achieve this:
Custom Poison Encoders
Implement the Poison.Encoder
protocol for your model. eg
defimpl Poison.Encoder, for: ProjectStatus.StatusEmail do
def encode(model, opts) do
model
|> Map.take([:name, :id, :email, :subject, :content,
:project_id, :sent_date, :status_date])
|> Poison.Encoder.encode(opts)
end
end
Whenever the model is sent as payload on a channel (or as a JSON Object via a view) it will serialise to just those selected fields.
Select the fields in the channel
Alternatively you might want to save bandwidth by providing just those fields required by the client. This tightly couples the Phoenix Channel code to the client, but you might argue that it's tightly coupled in any case.
eg if the client only needs the model's id
and subject
fields.
def handle_in("get_project_status_emails", %{}, socket) do
status_emails = socket.assigns[:project_id]
|> ProjectEmailing.project_status_emails
|> Enum.map(&(Map.take(&1, [:id, :subject])))
{:reply, {:ok, %{status_emails: status_emails}}, socket}
end
Right now I am going with the simpler first approach, custom encoders; I'll worry about bandwidth when it becomes a problem.