Phoenix.Sync.Controller (Phoenix.Sync v0.6.0)
View SourceProvides controller-level integration with sync streams.
Unlike Phoenix.Sync.Router.sync/2
, which only permits static shape
definitions, in a controller you can use request and session information to
filter your data.
Phoenix Example
defmodule MyAppWeb.TodoController do
use Phoenix.Controller, formats: [:html, :json]
import Elixir.Phoenix.Sync.Controller
alias MyApp.Todos
def all(conn, %{"user_id" => user_id} = params) do
sync_render(
conn,
params,
from(t in Todos.Todo, where: t.owner_id == ^user_id)
)
end
end
Plug Example
You should use Elixir.Phoenix.Sync.Controller
in your Plug.Router
, then within your route
you can use the sync_render/2
function.
defmodule MyPlugApp.Router do
use Plug.Router, copy_opts_to_assign: :options
use Elixir.Phoenix.Sync.Controller
plug :match
plug :dispatch
get "/todos" do
sync_render(conn, MyPlugApp.Todos.Todo)
end
end
Shape definitions
See Phoenix.Sync.shape!/2
for examples of shape definitions
Interruptible requests
There may be circumstances where shape definitions are dynamic based on, say,
a database query. In this case you should wrap your shape definitions in a
function and use sync_render/3
so that changes to clients' shapes can be.
immediately picked up by the clients.
For more information see Phoenix.Sync.Controller.sync_render/3
and
Phoenix.Sync.interrupt/2
.
Summary
Types
@type shape() :: shape_options() | Electric.Client.ecto_shape()
@type shape_option() :: Phoenix.Sync.PredefinedShape.option()
@type shape_options() :: [shape_option()]
Functions
@spec sync_render( Plug.Conn.t(), Plug.Conn.params(), (-> Phoenix.Sync.PredefinedShape.t() | Phoenix.Sync.PredefinedShape.shape()) ) :: Plug.Conn.t()
Return the sync events for the given shape with an interruptible response.
By passing the shape definition as a function you enable interruptible requests. This is useful when the shape definition is dynamic and may change.
By interrupting the long running requests to the sync API, changes to the client's shape can be picked up immediately without waiting for the long-poll timeout to expire.
For instance, when creating a task manager apps your clients will have a list of tasks and each task will have a set of steps.
So the controller code the steps
sync endpoint might look like this:
def steps(conn, %{"user_id" => user_id} = params) do
task_ids =
from(t in Tasks.Task, where: t.user_id == ^user_id, select: t.id)
|> Repo.all()
steps_query =
Enum.reduce(
task_ids,
from(s in Tasks.Step),
fn query, task_id -> or_where(query, [s], s.task_id == ^task_id) end
)
sync_render(conn, params, steps_query)
end
This works but when the user adds a new task, existing requests from clients won't pick up new tasks until active long-poll requests complete, which means that new tasks may not appear in the page until up to 20 seconds later.
To handle this situation you can make your sync_render/3
call interruptible
like so:
def steps(conn, %{"user_id" => user_id} = params) do
sync_render(conn, params, fn ->
task_ids =
from(t in Tasks.Task, where: t.user_id == ^user_id, select: t.id)
|> Repo.all()
Enum.reduce(
task_ids,
from(s in Tasks.Step),
fn query, task_id -> or_where(query, [s], s.task_id == ^task_id) end
)
end)
end
And add an interrupt call in your tasks controller to trigger the interrupt:
def create(conn, %{"user_id" => user_id, "task" => task_params}) do
# create the task as before...
# interrupt all active steps shapes
Phoenix.Sync.interrupt(Tasks.Step)
# return the response...
end
Now active long-poll requests to the steps
table will be interrupted and
re-tried and clients will receive the updated shape data including the new
task immediately.
If you want to use keyword-based shapes instead of Ecto queries or add
options to Ecto shapes, you can use Phoenix.Sync.shape!/2
in the shape
definition function:
sync_render(conn, params, fn ->
Phoenix.Sync.shape!(query, replica: :full)
end)
@spec sync_render(Plug.Conn.t(), Plug.Conn.params(), shape(), shape_options()) :: Plug.Conn.t()
Return the sync events for the given shape as a Plug.Conn
response.