{"id":16488834,"url":"https://github.com/athal7/dvr","last_synced_at":"2025-10-21T14:44:29.790Z","repository":{"id":32966070,"uuid":"148040342","full_name":"athal7/dvr","owner":"athal7","description":"Record and replay your Phoenix channels","archived":true,"fork":false,"pushed_at":"2024-12-24T18:55:02.000Z","size":229,"stargazers_count":10,"open_issues_count":3,"forks_count":3,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-28T21:05:54.649Z","etag":null,"topics":["absinthe-graphql","elixir-phoenix","mnesia","websockets"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/athal7.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["athal7"]}},"created_at":"2018-09-09T15:45:19.000Z","updated_at":"2025-01-15T12:43:18.000Z","dependencies_parsed_at":"2023-02-17T23:01:08.360Z","dependency_job_id":"26cb6fc4-fdb2-40ad-bc52-99e9c78b4002","html_url":"https://github.com/athal7/dvr","commit_stats":{"total_commits":123,"total_committers":6,"mean_commits":20.5,"dds":"0.41463414634146345","last_synced_commit":"634f8888f56d42e5b89821800ce74259def937c1"},"previous_names":[],"tags_count":11,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/athal7%2Fdvr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/athal7%2Fdvr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/athal7%2Fdvr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/athal7%2Fdvr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/athal7","download_url":"https://codeload.github.com/athal7/dvr/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243910667,"owners_count":20367546,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["absinthe-graphql","elixir-phoenix","mnesia","websockets"],"created_at":"2024-10-11T13:40:02.594Z","updated_at":"2025-10-21T14:44:29.462Z","avatar_url":"https://github.com/athal7.png","language":"Elixir","funding_links":["https://github.com/sponsors/athal7"],"categories":[],"sub_categories":[],"readme":"# DVR\n\n*Record and replay your Phoenix channels*\n\n![Hex.pm](https://img.shields.io/hexpm/v/dvr.svg)\n![Hex.pm licence](https://img.shields.io/hexpm/l/dvr.svg)\n![Build Status)](https://img.shields.io/github/actions/workflow/status/athal7/dvr/ci.yml?branch=main)\n\n**Documentation can be found at [https://hexdocs.pm/dvr](https://hexdocs.pm/dvr).**\n\nDVR 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.\n\n## Installation\n\nThe package can be installed by adding `dvr` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:dvr, \"~\u003e 1.1.0\"}\n  ]\nend\n```\n\n## Configuration\n\n### Mnesia\n\nThe mnesia table can be easily setup across your cluster by utilizing the [mnesiac](https://github.com/beardedeagle/mnesiac) package.\n\n```elixir\nconfig :mnesiac, stores: [DVR.Store]\n```\n\nOr you can configure the mnesia table on your own at startup:\n\n```elixir\nDVR.Store.init_store()\nDVR.Store.copy_store()\n```\n\n### Cleanup\n\nYou 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:\n\n```elixir\nchildren = [DVR.Cleanup]\nSupervisor.start_link(children, strategy: :one_for_one)\n```\n\nThe default interval is 1 minute, and the default ttl is 1 hour, but you can configure them as you desire:\n\n```elixir\nchildren = [{DVR.Cleanup, interval_seconds: 60 * 10, ttl_seconds: 60 * 60 * 24}]\nSupervisor.start_link(children, strategy: :one_for_one)\n```\n\n## Usage\n\n### Basic Usage\n\n**Record a message**\n\n```elixir\n{:ok, id} = DVR.record(%{some: \"message\"}, ['some_topic'])\n```\n\n**Replay missed messages**\n\n```elixir\nid # last seen message id\n|\u003e DVR.replay(['some_topic'])\n|\u003e Stream.each(\u0026send_to_client/1) # your implementation\n|\u003e Stream.run()\n```\n\n**Check for a message by id**\n\n```elixir\n{:ok, id} = DVR.search(id)\n```\n\n### With Phoenix\n\nIn `channel.ex`\n\n```elixir\ndefmodule MyApp.Channel do\n  use Phoenix.Channel\n  use DVR.Channel\n\n  ...\n\n  def handle_in(\"new_msg\", msg, socket) do\n    case DVR.record(msg, [socket.topic]) do\n      {:ok, replay_id} -\u003e\n        broadcast!(socket, socket.topic, Map.put(msg, :replay_id, replay_id))\n\n      err -\u003e\n        Logger.error(\"Unable to add replayId to message\", error: err)\n        push(socket, socket.topic, msg)\n    end\n\n    {:noreply, socket}\n  end\nend\n```\n\nIn your client:\n\n```js\n...\n\nlet replayId // recovered from storage somewhere\n\nchannel.on(\"new_msg\", payload =\u003e {\n  lastMessageId = payload.replay_id\n})\n\nchannel.join()\n  .receive(\"ok\", resp =\u003e {\n    console.log(\"Joined successfully\", resp)\n    channel.push('replay', { replayId })\n  })\n  .receive(\"error\", resp =\u003e { console.log(\"Unable to join\", resp) })\n```\n\n### With Absinthe\n\nMake sure to add the `replayId` to your schema for the subscription type that you are publishing. Then you can record the message when resolving:\n\n```elixir\nobject :foo do\n  field(:bar, :string)\n  field(:baz, :string)\nend\n\nobject :foo_update do\n  field(:foo, :foo)\n  field(:replayId, :integer)\nend\n\nsubscription do\n  field :foo_updates, :foo_update do\n    config(fn _, _ -\u003e {:ok, topic: \"*\"} end)\n\n    resolve(fn root, _, _ -\u003e\n      {:ok, replay_id} = DVR.record(root, [foo_updates: \"*\"])\n      {:ok, Map.put(root, :replay_id, replay_id)}\n    end)\n  end\nend\n```\n\nFor now, you have to customize the entire set of channel / socket modules, since there's not yet a way to decorate the default channel:\n\nendpoint.ex\n\n```elixir\ndefmodule MyApp.Endpoint do\n  use Phoenix.Endpoint, otp_app: :web\n  use Absinthe.Phoenix.Endpoint\n\n  socket(\"/socket\", MyApp.UserSocket, websocket: true)\n  ...\n```\n\nsocket.ex\n\n```elixir\ndefmodule MyApp.UserSocket do\n  use Phoenix.Socket\n\n  def connect(_payload, socket), do: {:ok, socket}\n  def id(_socket), do: nil\n\n  channel(\n    \"__absinthe__:*\",\n    MyApp.AbsintheChannel,\n    assigns: %{__absinthe_schema__: MyApp.Schema}\n  )\n\n  defdelegate put_options(socket, opts), to: Absinthe.Phoenix.Socket\n  defdelegate put_schema(socket, schema), to: Absinthe.Phoenix.Socket\nend\n```\n\nchannel.ex\n\n```elixir\ndefmodule MyApp.Channel do\n  use Phoenix.Channel\n\n  defdelegate handle_in(event, payload, socket), to: DVR.AbsintheChannel\n  defdelegate join(channel, message, socket), to: Absinthe.Phoenix.Channel\nend\n```\n\nIn your client:\n\n```js\n...\n\nlet replayId // recovered from storage somewhere\n\nchannel.on(\"new_msg\", payload =\u003e {\n  // take the replayId from the relevant place in your schema\n  replayId = payload.replayId\n})\n\nchannel.join()\n  .receive(\"ok\", resp =\u003e {\n    console.log(\"Joined successfully\", resp)\n    const subscriptionId = resp.body.payload.response.subscriptionId\n    channel.push('replay', { replayId, subscriptionId })\n  })\n  .receive(\"error\", resp =\u003e { console.log(\"Unable to join\", resp) })\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fathal7%2Fdvr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fathal7%2Fdvr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fathal7%2Fdvr/lists"}