Phoenix.Sync (Phoenix.Sync v0.5.1)

View Source

Real-time sync for Postgres-backed Phoenix applications.

See the docs for more information.

Summary

Functions

Interrupts all long-polling requests matching the given shape definition.

Returns the required adapter configuration for your Phoenix Endpoint or Plug.Router.

Returns a shape definition for the given params.

Types

match_shape_params()

@type match_shape_params() :: %{
  table: String.t(),
  namespace: nil | String.t(),
  where: nil | String.t(),
  params: nil | %{required(String.t()) => String.t()},
  columns: nil | [String.t(), ...]
}

param_override()

@type param_override() ::
  {:namespace, String.t()}
  | {:table, String.t()}
  | {:where, String.t()}
  | {:columns, String.t()}

param_overrides()

@type param_overrides() :: [param_override()]

queryable()

@type queryable() :: Ecto.Queryable.t() | Ecto.Schema.t() | Ecto.Changeset.t()

shape_definition()

@type shape_definition() :: String.t() | queryable() | shape_specification()

shape_specification()

@type shape_specification() :: [
  table: binary(),
  where: nil | binary(),
  columns: nil | [binary()],
  namespace: nil | binary(),
  params:
    nil
    | %{optional(pos_integer()) => binary() | integer() | float() | boolean()}
    | [binary() | integer() | float() | boolean()],
  replica: term()
]

Functions

client!()

See Phoenix.Sync.Client.new!/0.

interrupt(shape, shape_opts \\ [])

@spec interrupt(
  shape_definition() | (match_shape_params() -> boolean()),
  shape_specification()
) ::
  {:ok, non_neg_integer()}

Interrupts all long-polling requests matching the given shape definition.

The broader the shape definition, the more requests will be interrupted.

Returns the number of interrupted requests.

Examples

To interrupt all shapes on the todos table:

Phoenix.Sync.interrupt("todos")
Phoenix.Sync.interrupt(table: "todos")

or the same using an Ecto.Schema module:

Phoenix.Sync.interrupt(Todos.Todo)

all shapes with the given parameterized where clause:

Phoenix.Sync.interrupt(table: "todos", where: "user_id = $1")

or a single shape for the given user:

Phoenix.Sync.interrupt(
  from(t in Todos.Todo, where: t.user_id == ^user_id)
)

# or

Phoenix.Sync.interrupt(
  table: "todos",
  where: "user_id = $1",
  params: [user_id]
)

# or

Phoenix.Sync.interrupt(
  table: "todos",
  where: "user_id = '#{user_id}'"
)

If you want more control over the match, you can pass a function that will receive a normalized shape definition and should return true if the active shape matches.

  Phoenix.Sync.interrupt(fn %{table: _, where: _, params: _} = shape ->
    shape.table == "todos" &&
      shape.where == "user_id = $1" &&
      shape.params["0"] == user_id
  end)

The normalized shape argument is a map with the following keys:

  • table, e.g. "todos"
  • namespace, e.g. "public"
  • where, e.g. "where user_id = $1"
  • params, a map of argument position to argument value, e.g. %{"0" => "true", "1" => "..."}
  • columns, e.g. ["id", "title"]

All except table may be nil.

Interrupting Ecto Query-based Shapes

Be careful when mixing Ecto query-based shapes with interrupt calls using hand-written where clauses.

The shape

Phoenix.Sync.Controller.sync_stream(conn, params, fn ->
  from(t in Todos.Todo, where: t.user_id == ^user_id)
end)

will not be interrupted by

Phoenix.Sync.interrupt(
  table: "todos",
  where: "user_id = '#{user_id}'"
)

because the where clause matching is a simple exact string match and Ecto query generated where clauses will generally be different from the equivalent hand-written version. If you want to interrupt a query-based shape you should use the same query as the interrupt criteria.

Writing interrupts

It's better to be too broad with your interrupt calls than too narrow. Only clients whose shape definition changes after the interrupt/1 call will be affected.

Supported options

The more options you give the more specific the interrupt call will be. Only the table name is required.

  • table - Required. Interrupts all shapes matching the given table. E.g. "todos"
  • namespace - The table namespace. E.g. "public"
  • where - The shape's where clause. Can in be parameterized and will match all shapes with the same where filter irrespective of the parameters (unless provided). E.g. "status = $1", "completed = true"
  • columns - The columns included in the shape. E.g. ["id", "title", "completed"]
  • params - The values associated with a parameterized where clause. E.g. [true, 1, "alive"], %{1 => true}

plug_opts()

Returns the required adapter configuration for your Phoenix Endpoint or Plug.Router.

Phoenix

Configure your endpoint with the configuration at runtime by passing the phoenix_sync configuration to your endpoint in the Application.start/2 callback:

def start(_type, _args) do
  children = [
    # ...
    {MyAppWeb.Endpoint, phoenix_sync: Phoenix.Sync.plug_opts()}
  ]
end

Plug

Add the configuration to the Plug opts in your server configuration:

children = [
  {Bandit, plug: {MyApp.Router, phoenix_sync: Phoenix.Sync.plug_opts()}}
]

Your Plug.Router must be configured with copy_opts_to_assign and you should use the rele

defmodule MyApp.Router do
  use Plug.Router, copy_opts_to_assign: :options

  use Phoenix.Sync.Controller
  use Phoenix.Sync.Router

  plug :match
  plug :dispatch

  sync "/shapes/todos", Todos.Todo

  get "/shapes/user-todos" do
    %{"user_id" => user_id} = conn.params
    sync_render(conn, from(t in Todos.Todo, where: t.owner_id == ^user_id)
  end
end

shape!(shape, shape_opts \\ [])

Returns a shape definition for the given params.

Examples

  • An Ecto.Schema module:

    Phoenix.Sync.shape!(MyPlugApp.Todos.Todo)
  • An Ecto query:

    Phoenix.Sync.shape!(from(t in Todos.Todo, where: t.owner_id == ^user_id))
  • A changeset/1 function which defines the table and columns:

    Phoenix.Sync.shape!(&Todos.Todo.changeset/1)
  • A changeset/1 function plus a where clause:

    Phoenix.Sync.shape!(
      &Todos.Todo.changeset/1,
      where: "completed = false"
    )

    or a parameterized where clause:

    Phoenix.Sync.shape!(
      &Todos.Todo.changeset/1,
      where: "completed = $1", params: [false]
    )
  • A keyword list defining the shape parameters:

    Phoenix.Sync.shape!(
      table: "todos",
      namespace: "my_app",
      where: "completed = $1",
      params: [false]
    )

Options

When defining a shape via a keyword list, it supports the following options:

  • :table (String.t/0) - Required.

  • :where - Filter the table according to the where clause. The default value is nil.

  • :columns - List of columns to include in the shape. Must include all primary keys. If nil this is equivalent to all columns (SELECT *) The default value is nil.

  • :namespace - The namespace the table belongs to. If nil then Postgres will use whatever schema is the default (usually public). The default value is nil.

  • :params - Values of positional parameters in the where clause. These will substitute $i placeholder in the where clause. The default value is nil.

  • :replica - Modifies the data sent in update and delete change messages.

    When set to :full the entire row will be sent for updates and deletes, not just the changed columns.

    The default value is :default.