Phoenix.Sync.Controller (Phoenix.Sync v0.6.0)

View Source

Provides 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

Functions

Return the sync events for the given shape with an interruptible response.

Return the sync events for the given shape as a Plug.Conn response.

Types

shape()

@type shape() :: shape_options() | Electric.Client.ecto_shape()

shape_option()

@type shape_option() :: Phoenix.Sync.PredefinedShape.option()

shape_options()

@type shape_options() :: [shape_option()]

Functions

sync_render(conn, params, shape_fun)

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)

sync_render(conn, params, shape, shape_opts \\ [])

@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.