defmodule LiveSync do @moduledoc ~S""" LiveSync is the core of all interactions include a postgres replication process and LiveView hooks to automatically sync data to the client. ## Installation Add `live_sync` to the list of dependencies in `mix.exs`: def deps do [ {:live_sync, "~> 0.1.0"} ] end Add a migration to setup replication: defmodule MyApp.Repo.Migrations.SetupLiveSync do use Ecto.Migration def up do LiveSync.Migration.up() end def down do LiveSync.Migration.down() end end Add `LiveSync` to your supervision tree: * `repo` (required): The Ecto repo to use for replication. * `otp_app` (required): The OTP app to use to lookup schemas deriving the watch protocol. defmodule MyApp.Application do @moduledoc false use Application @impl true def start(_type, _args) do children = [ ... {LiveSync, [repo: MyApp.Repo, otp_app: :my_app]} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end ## Usage For any Ecto schemas you want to watch, add the `LiveSync.Watch` derive: * `id` (optional): The primary key on the schema, defaults to `:id`. * `subscription_key` (required): The field on the schema that is used to filter messages before sending to the client. * `table` (optional): The table to watch, defaults to the schema's table name, if using a view you need to specify the table name defmodule MyApp.MyObject do use Ecto.Schema @derive {LiveSync.Watch, [ subscription_key: :organization_id, table: "objects" ]} schema "visible_objects" do field :name, :string field :organization_id, :integer end ... end Add the `LiveSync` macro to any LiveView module you want to automatically sync data: * `subscription_key` (required): This is the key that MUST exist in the assigns of the LiveView and is used for the subscription. This value must match what is in the schema's `@derive` attribute. * `watch` (required): A list of keys in assigns that should be watched. You may optionally specify a tuple with options. Schema is required for lists of objects to support inserting. use LiveSync, subscription_key: :organization_id, watch: [ :single_object, list_of_objects: [schema: MyApp.MyObject] ] ## Handling Sync Events An optional callback can also be added to the LiveView module to handle the updated data. This callback will be called for each assign key that is watched and changed. It must return the socket with updated assigns. def sync(:list_of_objects, updated, socket) do updates = updated |> Enum.filter(&is_nil(&1.executed_at)) |> Enum.sort_by(& &1.name) |> Repo.preload([...]) assign(socket, list_of_objects: updates) end """ use Supervisor import Phoenix.LiveView def start_link(opts) do Supervisor.start_link(__MODULE__, opts, name: __MODULE__) end @impl true def init(opts) do repo = Keyword.fetch!(opts, :repo) otp_app = Keyword.fetch!(opts, :otp_app) children = [ {Registry, name: LiveSync.Registry, keys: :duplicate}, {LiveSync.Replication, [name: LiveSync.Replication, otp_app: otp_app] ++ repo.config()}, {Task, fn -> LiveSync.Replication.wait_for_connection!(LiveSync.Replication) end} ] Supervisor.init(children, strategy: :one_for_one) end defmacro __using__(opts) do quote do on_mount({LiveSync, unquote(opts)}) def sync(key, value, socket), do: assign(socket, key, value) @before_compile {LiveSync, :add_sync_fallback} defoverridable sync: 3 end end @doc false defmacro add_sync_fallback(_env) do quote do def sync(key, value, socket), do: assign(socket, key, value) end end @doc false def on_mount(opts, _params, _session, socket) do socket = socket |> attach_hook(:live_sync, :handle_params, fn _params, _uri, socket -> if connected?(socket) do subscription_key = socket.assigns[opts[:subscription_key]] LiveSync.Replication.subscribe("live_sync:#{subscription_key}") end {:cont, socket} end) |> attach_hook(:sync, :handle_info, fn msg, socket -> LiveSync.Socket.handle_info(msg, socket, opts) end) {:cont, socket} end def opts(module) do LiveSync.Watch.impl_for(struct(module)).opts() end @doc """ Returns the info of how to compare the given struct for updates. iex> LiveSync.lookup_info(%MyObject{id: 1, organization_id: 2}) {MyObject, 1} """ def lookup_info(struct) do case LiveSync.Watch.impl_for(struct) do nil -> nil impl -> impl.info(struct) end end @doc """ Returns the value for the subscription key for the given struct iex> LiveSync.subscription_key(%MyObject{id: 1, organization_id: 2}) 2 """ def subscription_key(struct) do impl = LiveSync.Watch.impl_for(struct) impl.subscription_key(struct) end end