Phoenix.Sync.Controller (Phoenix.Sync v0.6.1)
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
endPlug 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
endShape 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)
endThis 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)
endAnd 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...
endNow 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.