View Source AshPhoenix.LiveView (ash_phoenix v2.1.8)
Utilities for keeping Ash query results up to date in a LiveView.
Summary
Functions
Incorporates an Ash.Notifier.Notification
into the query results, based on the liveness configuration.
Runs the callback, and stores the information required to keep it live in the socket assigns.
Types
@type assign() :: atom()
@type assigns() :: map()
@type callback() :: (socket() -> callback_result()) | (socket(), Keyword.t() | nil -> callback_result())
@type callback_result() :: struct() | [struct()] | Ash.Page.page() | nil
@type liveness_options() :: Keyword.t()
@type socket() :: term()
@type topic() :: String.t()
Functions
Incorporates an Ash.Notifier.Notification
into the query results, based on the liveness configuration.
You will want to match on receiving a notification from Ash, and the easiest way to do that is to match on the payload like so:
@impl true
def handle_info(%{topic: topic, payload: %Ash.Notifier.Notification{}}, socket) do
{:noreply, handle_live(socket, topic, [:query1, :query2, :query3])}
end
Feel free to intercept notifications and do your own logic to respond to events. Ultimately, all
that matters is that you also call handle_live/3
if you want it to update your query results.
The assign or list of assigns passed as the third argument must be the same names passed into
keep_live
. If you only want some queries to update based on some events, you can define multiple
matches on events, and only call handle_live/3
with the assigns that should be updated for that
notification.
@spec keep_live(socket(), assign(), callback(), liveness_options()) :: socket()
Runs the callback, and stores the information required to keep it live in the socket assigns.
The data will be assigned to the provided key, e.g keep_live(socket, :me, ...)
would assign the results
to :me
(accessed as @me
in the template).
Additionally, you'll need to define a handle_info/2
callback for your liveview to receive any
notifications, and pass that notification into handle_live/3
. See handle_live/3
for more.
Important
The logic for handling events to keep data live is currently very limited. It will simply rerun the query every time. To this end, you should feel free to intercept individual events and handle them yourself for more optimized liveness.
Pagination
To make paginated views convenient, as well as making it possible to keep those views live, Ash does not
simply rerun the query when it gets an update, as that could involve shuffling the records around on the
page. Eventually this will be configurable, but for now, Ash simply adjusts the query to only include the
records that are on the page. If a record would be removed from a page due to a data change, it will simply
be left there. For the best performance, use keyset
pagination. If you need the ability to jump to a
page by number, you'll want to use offset
pagination, but keep in mind that it performs worse on large
tables.
To support this, accept a second parameter to your callback function, which will be the options to use in page_opts
Options:
:subscribe
- A topic or list of topics that should cause this data to update.:refetch?
(boolean/0
) - A boolean flag indicating whether a refetch is allowed to happen. Defaults totrue
:after_fetch
(term/0
) - A two argument function that takes the results, and the socket, and returns the new socket. Can be used to set assigns based on the result of the query.:results
- For list and page queries, by default the records shown are never changed (unless the page changes) Valid values are :keep, :lose The default value is:keep
.:load_until_connected?
(boolean/0
) - If the socket is not connected, then the value of the provided assign is set to:loading
. Has no effect ifinitial
is provided.:initial
(term/0
) - Results to use instead of running the query immediately.:refetch_interval
(non_neg_integer/0
) - An interval (in ms) to periodically refetch the query:refetch_window
(non_neg_integer/0
) - The minimum time (in ms) between refetches, including refetches caused by notifications.
A great way to get readable millisecond values is to use the functions in erlang's :timer
module,
like :timer.hours/1
, :timer.minutes/1
, and :timer.seconds/1
refetch_interval
If this option is set, a message is sent as {:refetch, assign_name, opts}
on that interval.
You can then match on that event, like so:
def handle_info({:refetch, assign, opts}, socket) do
{:noreply, handle_live(socket, :refetch, assign, opts)}
end
This is the equivalent of :timer.send_interval(interval, {:refetch, assign, opts})
, so feel free to
roll your own solution if you have complex refetching requirements.
refetch_window
Normally, when a pubsub message is received the query is rerun. This option will cause the query to wait at least
this amount of time before doing a refetch. This is accomplished with Process.send_after/4
, and recording the
last time each query was refetched. For example if a refetch happens at time 0
, and the refetch_window
is
10,000 ms, we would refetch, and record the time. Then if another refetch should happen 5,000 ms later, we would
look and see that we need to wait another 5,000ms. So we use Process.send_after/4
to send a
{:refetch, assign, opts}
message in 5,000ms. The time that a refetch was requested is tracked, so if the
data has since been refetched, it won't be refetched again.
Future Plans
One interesting thing here is that, given that we know the scope of data that a resource cares about, we should be able to make optimizations to this code, to support partial refetches, or even just updating the data directly. However, this will need to be carefully considered, as the risks involve showing users data they could be unauthorized to see, or having state in the socket that is inconsistent.