README.md

# DVR

*Record and replay your Phoenix channels*

![Hex.pm](https://img.shields.io/hexpm/v/dvr.svg)
![Hex.pm licence](https://img.shields.io/hexpm/l/dvr.svg)
![CircleCI Master](https://img.shields.io/circleci/project/github/athal7/dvr/master.svg)

**Documentation can be found at [https://hexdocs.pm/dvr](https://hexdocs.pm/dvr).**

DVR gives you the ability to resend channel messages from your Phoenix server, based on a client-supplied id for the last seen message. Unlike the [example mentioned in the Phoenix Docs](https://hexdocs.pm/phoenix/channels.html#resending-server-messages), this implementation utilizes [mnesia](http://erlang.org/doc/man/mnesia.html), as opposed to an external database backend.

## Installation

The package can be installed by adding `dvr` to your list of dependencies in `mix.exs`:

```elixir
def deps do
  [
    {:dvr, "~> 1.0.0"}
  ]
end
```

## Configuration

### Mnesia

The mnesia table can be easily setup across your cluster by utilizing the [mnesiac](https://github.com/beardedeagle/mnesiac) package.

```elixir
config :mnesiac, stores: [DVR.Store]
```

Or you can configure the mnesia table on your own at startup:

```elixir
DVR.Store.init_store()
DVR.Store.copy_store()
```

### Cleanup

You probably want to cleanup the saved messages after a period of time, so as to not over-use your memory or disc capacity (based on the mnesia backend you choose). You can do so by adding the provided cleanup task to your supervision tree:

```elixir
children = [DVR.Cleanup]
Supervisor.start_link(children, strategy: :one_for_one)
```

The default interval is 1 minute, and the default ttl is 1 hour, but you can configure them as you desire:

```elixir
children = [{DVR.Cleanup, interval_seconds: 60 * 10, ttl_seconds: 60 * 60 * 24}]
Supervisor.start_link(children, strategy: :one_for_one)
```

## Usage

### Basic Usage

**Record a message**

```elixir
{:ok, id} = DVR.record(%{some: "message"}, ['some_topic'])
```

**Replay missed messages**

```elixir
id # last seen message id
|> DVR.replay(['some_topic'])
|> Stream.each(&send_to_client/1) # your implementation
|> Stream.run()
```

**Check for a message by id**

```elixir
{:ok, id} = DVR.search(id)
```

### With Phoenix

In `channel.ex`

```elixir
defmodule MyApp.Channel do
  use Phoenix.Channel
  use DVR.Channel

  ...

  def handle_in("new_msg", msg, socket) do
    case DVR.record(msg, [socket.topic]) do
      {:ok, replay_id} ->
        broadcast!(socket, socket.topic, Map.put(msg, :replay_id, replay_id))

      err ->
        Logger.error("Unable to add replayId to message", error: err)
        push(socket, socket.topic, msg)
    end

    {:noreply, socket}
  end
end
```

In your client:

```js
...

let replayId // recovered from storage somewhere

channel.on("new_msg", payload => {
  lastMessageId = payload.replay_id
})

channel.join()
  .receive("ok", resp => {
    console.log("Joined successfully", resp)
    channel.push('replay', { replayId })
  })
  .receive("error", resp => { console.log("Unable to join", resp) })
```

### With Absinthe

Make sure to add the `replayId` to your schema for the subscription type that you are publishing. Then you can record the message when resolving:

```elixir
object :foo do
  field(:bar, :string)
  field(:baz, :string)
end

object :foo_update do
  field(:foo, :foo)
  field(:replayId, :integer)
end

subscription do
  field :foo_updates, :foo_update do
    config(fn _, _ -> {:ok, topic: "*"} end)

    resolve(fn root, _, _ ->
      {:ok, replay_id} = DVR.record(root, [foo_updates: "*"])
      {:ok, Map.put(root, :replay_id, replay_id)}
    end)
  end
end
```

For now, you have to customize the entire set of channel / socket modules, since there's not yet a way to decorate the default channel:

endpoint.ex

```elixir
defmodule MyApp.Endpoint do
  use Phoenix.Endpoint, otp_app: :web
  use Absinthe.Phoenix.Endpoint

  socket("/socket", MyApp.UserSocket, websocket: true)
  ...
```

socket.ex

```elixir
defmodule MyApp.UserSocket do
  use Phoenix.Socket

  def connect(_payload, socket), do: {:ok, socket}
  def id(_socket), do: nil

  channel(
    "__absinthe__:*",
    MyApp.AbsintheChannel,
    assigns: %{__absinthe_schema__: MyApp.Schema}
  )

  defdelegate put_opts(socket, opts), to: Absinthe.Phoenix.Socket
  defdelegate put_schema(socket, schema), to: Absinthe.Phoenix.Socket
end
```

channel.ex

```elixir
defmodule MyApp.Channel do
  use Phoenix.Channel

  defdelegate handle_in(event, payload, socket), to: DVR.AbsintheChannel
  defdelegate join(channel, message, socket), to: Absinthe.Phoenix.Channel
end
```

In your client:

```js
...

let replayId // recovered from storage somewhere

channel.on("new_msg", payload => {
  // take the replayId from the relevant place in your schema
  replayId = payload.replayId
})

channel.join()
  .receive("ok", resp => {
    console.log("Joined successfully", resp)
    const subscriptionId = resp.body.payload.response.subscriptionId
    channel.push('replay', { replayId, subscriptionId })
  })
  .receive("error", resp => { console.log("Unable to join", resp) })
```