lib/live_data.ex

defmodule LiveData do
  @moduledoc """
  LiveData makes it easy to synchronize a JSON data structure from the server
  to a client. Changes to the data structure over time is streamed as minimal
  diffs.

  LiveData aims to provide a programming model similar to LiveView, but for
  arbitrary data structures instead of only HTML.

  You write a `render` function in your LiveData, which takes any assigns you
  have set as an argument. Within the `render` function you build the data
  structure you want synchronized to the client. As you update the `assigns`,
  the LiveData library will take care of calling your `render` function,
  calculating diffs, and synchronizing to the client.

  As with LiveView, your LiveData is just a normal process. The programming
  model should feel very familear if you have ever used LiveView before.

  ## Live-cycle
  Unlike with LiveView, the lifecycle of a LiveData is not tied closely to
  any given HTTP request.

  In order to open a connection to a LiveData from the client, you would
  start out by making a connection to the LiveData socket endpoint.

  On the LiveData socket connection, the client can then connect to any
  number of LiveDatas.

  A LiveData ends when either:
   * The client closes the LiveData socket connection. This will cause all
     LiveDatas that live on that socket to close.
   * The client closes the individual LiveData. This will cause any other
     LiveDatas on the socket to remain open.
   * The LiveData crashes or disconnects from the server side.

  On a server-side crash or disconnect, it is up to the client to decide
  how to handle it. A common behaviour might be to reconnect.

  ## Keys
  Using keys are very important if you want the LiveData library to generate
  efficient diffs for you.

  Keys allow you to give the LiveData library a hint that it can identify a
  piece of data uniquely by a given identifier.

  As a rule of thumb, you should use a key whenever are looping over and
  generating output from something dynamic.

  ### Example
  Say you have the following LiveData which returns a list of users:

  ```elixir
  deft render(assigns) do
    for user <- assigns[:users] do
      %{
        id: user.id,
        name: user.name,
        [...]
      }
    end
  end
  ```

  Because LiveData has no way of knowing which parts of your data structure
  it can use as a comparison key when diffing, this would produce inefficient
  diffs.

  The data for users can be uniquely identified by `user.id`. We can inform
  LiveData of that fact by doing the following:

  ```elixir
  deft render(assigns) do
    for user <- assigns[:users] do
      keyed user.id, %{
        id: user.id,
        name: user.name,
        [...]
      }
    end
  end
  ```

  This will make LiveData produce efficient diffs whether the users in
  the list change order, any individual properties change, or other changes.
  """

  alias LiveData.{Socket, Tracked}

  @type rendered :: any()

  @callback mount(params :: any(), Socket.t()) :: {:ok, Socket.t()}

  @callback handle_event(event :: any(), Socket.t()) :: {:ok, Socket.t()}
  @callback handle_info(message :: any(), Socket.t()) :: {:ok, Socket.t()}

  @callback render(Socket.assigns()) :: rendered()
  @callback __tracked__render__(Socket.assigns()) :: Tracked.tree()

  @optional_callbacks mount: 2, handle_event: 2, handle_info: 2

  defmacro __using__(opts) do
    quote bind_quoted: [opts: opts] do
      use LiveData.Tracked
      import LiveData
      @behaviour LiveData
    end
  end

  def assign(%Socket{assigns: assigns} = socket, key, value) do
    assigns = Map.put(assigns, key, value)
    %{socket | assigns: assigns}
  end

  def debug_prints?, do: false
end