{"id":32167059,"url":"https://github.com/stepnivlk/pushest","last_synced_at":"2026-02-20T00:31:54.703Z","repository":{"id":56412277,"uuid":"126724945","full_name":"stepnivlk/pushest","owner":"stepnivlk","description":"Bidirectional Pusher client in Elixir","archived":false,"fork":false,"pushed_at":"2018-04-28T10:56:50.000Z","size":129,"stargazers_count":35,"open_issues_count":0,"forks_count":6,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-08-10T19:06:58.657Z","etag":null,"topics":["elixir","genserver","pusher","websockets"],"latest_commit_sha":null,"homepage":"","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stepnivlk.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2018-03-25T18:00:54.000Z","updated_at":"2023-11-21T14:26:06.000Z","dependencies_parsed_at":"2022-08-15T18:10:51.283Z","dependency_job_id":null,"html_url":"https://github.com/stepnivlk/pushest","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"purl":"pkg:github/stepnivlk/pushest","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepnivlk%2Fpushest","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepnivlk%2Fpushest/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepnivlk%2Fpushest/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepnivlk%2Fpushest/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stepnivlk","download_url":"https://codeload.github.com/stepnivlk/pushest/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stepnivlk%2Fpushest/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29637409,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T22:32:43.237Z","status":"ssl_error","status_checked_at":"2026-02-19T22:32:38.330Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"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":["elixir","genserver","pusher","websockets"],"created_at":"2025-10-21T15:19:41.427Z","updated_at":"2026-02-20T00:31:54.698Z","avatar_url":"https://github.com/stepnivlk.png","language":"Elixir","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Pushest\n\nPushest is bidirectional Pusher client leveraging Elixir/OTP to combine server and client-side\nPusher features together in one library. Pushest communicates both via WebSockets and REST API.\nYou can trigger on any channel, subscribe to channels, handle events using callbacks or\nkeep track of presence.\n\n[![Build Status](https://travis-ci.org/stepnivlk/pushest.svg?branch=master)](https://travis-ci.org/stepnivlk/pushest) [![Ebert](https://ebertapp.io/github/stepnivlk/pushest.svg)](https://ebertapp.io/github/stepnivlk/pushest)\n\n## TODO\n- [x] Event scoping\n- [x] Presence\n- [x] Unsubscribe method\n- [x] Channels list method\n- [x] Auth token generated only for private/presence channels\n- [x] Missing tests\n- [x] Handle `pusher:error`\n- [x] Generate documentation\n- [x] :gun.conn monitoring\n- [x] start_link/3 - opts to Pushest\n- [x] Named process option\n- [x] Propagate app version to url\n- [ ] Overall error handling\n- [x] Publish to hex.pm\n- [x] Fallback to REST when triggering on a public channel\n- [ ] Test recovery from :gun_down / EXIT\n- [ ] expose `auth` function to generate a token for client-side libraries.\n- [ ] trigger batching\n- [ ] Push notifications\n- [x] Subscribe to a list of channels after startup\n- [ ] Full recovery after network outage, exit, etc. Buffer needed.\n- [ ] Refactor :gun.conn PID handling.\n- [ ] Add a support for testing of modules using Pushest\n\n## Usage\n### A simple implementation in an OTP application would be:\n```elixir\n# Add necessary pusher configuration to your application config:\n# simple_client/config/config.exs\nconfig :simple_client, SimpleClient,\n  pusher_app_id: System.get_env(\"PUSHER_APP_ID\"),\n  pusher_key: System.get_env(\"PUSHER_APP_KEY\"),\n  pusher_secret: System.get_env(\"PUSHER_SECRET\"),\n  pusher_cluster: System.get_env(\"PUSHER_CLUSTER\"),\n  pusher_encrypted: true\n\n# simple_client/simple_client.ex\ndefmodule SimpleClient do\n  use Pushest, otp_app: :simple_client\n\n  # Subscribe to these channels right after application startup.\n  def init_channels do\n    [\n      [name: \"public-init-channel\", user_data: %{}],\n      [name: \"private-init-channel\", user_data: %{}],\n      [name: \"presence-init-channel\", user_data: %{user_id: 123}],\n    ]\n  end\n\n  # handle_event/2 is user-defined callback which is triggered whenever an event\n  # occurs on the channel.\n  def handle_event({:ok, \"public-init-channel\", \"some-event\"}, frame) do\n    # do something with public-init-channel frame\n  end\n\n  def handle_event({:ok, \"public-channel\", \"some-event\"}, frame) do\n    # do something with public-channel frame\n  end\n\n  def handle_event({:ok, \"private-channel\", \"some-other-event\"}, frame) do\n    # do something with private-channel frame\n  end\n  \n  # We can also catch errors.\n  def handle_event({:error, msg}, frame) do\n    # do something with error\n  end\nend\n\n# Now you can start your application with Pushest as a part of your supervision tree:\n# simple_client/lib/simple_client/application.ex\ndef start(_type, _args) do\n  children = [\n    {SimpleClient, []}\n  ]\n\n  opts = [strategy: :one_for_one, name: Sup.Supervisor]\n  Supervisor.start_link(children, opts)\nend\n```\n\n### You can also provide Pusher options directly via start_link/1 (without using OTP app configuration):\n```elixir\nconfig = %{\n  app_id:  System.get_env(\"PUSHER_APP_ID\"),\n  key: System.get_env(\"PUSHER_APP_KEY\"),\n  secret: System.get_env(\"PUSHER_SECRET\"),\n  cluster: System.get_env(\"PUSHER_CLUSTER\"),\n  encrypted: true\n}\n\n{:ok, pid} = SimpleClient.start_link(config)\n```\n\n### Now you can use various functions injected in your module\n```elixir\nSimpleClient.channels()\n# =\u003e %{\n\"channels\" =\u003e %{\n  \"presence-init-channel\" =\u003e %{},\n  \"private-init-channel\" =\u003e %{},\n  \"public-init-channel\" =\u003e %{}\n}\n# ...\nSimpleClient.subscribe(\"public-channel\")\n:ok\n# ...\nSimpleClient.subscribe(\"private-channel\")\n:ok\n# ...\nSimpleClient.subscribe(\"presence-channel\", %{user_id: \"1\", user_info: %{name: \"Tomas\"}})\n:ok\n# ...\nSimpleClient.presence()\n%Pushest.Data.Presence{\n  count: 2,\n  hash: %{\"1\" =\u003e %{\"name\" =\u003e \"Tomas\"}, \"2\" =\u003e %{\"name\" =\u003e \"Jose\"}},\n  ids: [\"1\", \"2\"],\n  me: %{user_id: \"1\", user_info: %{name: \"Tomas\"}}\n}\n# ...\nSimpleClient.trigger(\"private-channel\", \"first-event\", %{message: \"Ahoj\"})\n:ok\n# ...\nSimpleClient.subscribed_channels()\n[\"presence-channel\", \"private-channel\", \"public-channel\",\n \"presence-init-channel\", \"private-init-channel\", \"public-init-channel\"]\n# ...\nSimpleClient.unsubscribe(\"public-channel\")\n:ok\n```\n\n### Functions list\n#### subscribe/1\nSubscribes to public or private channel\n```elixir\nSimpleClient.subscribe(\"public-channel\")\n:ok\n```\n\n#### subscribe/2\nSubscribes to private or presence channel with user data as second parameter.\nUser data has to contain `user_id` key with unique identifier for current user.\nCan optionally contain `user_info` field with map of additional informations about user.\n```elixir\nuser_data = %{user_id: 123, user_info: %{name: \"Tomas\", email: \"secret@secret.com\"}}\nSimpleClient.subscribe(\"presence-channel\", user_data)\n:ok\n# ...\nSimpleClient.subscribe(\"private-channel\", user_data)\n:ok\n```\n\n#### trigger/3\nTriggers on given channel and event with given data payload. Pushest sends data by\ndefault to REST API endpoint of Pusher, however when subscribed to private or presence channel\nit sends data to Pusher via WebSockets.\n```elixir\nSimpleClient.trigger(\"public-channel\", \"event\", %{message: \"message\"})\n:ok\n# ..\nSimpleClient.trigger(\"private-channel\", \"event\", %{message: \"message\"})\n:ok\n```\n\n#### trigger/4\nSame as `trigger/3` but lets you force trigger over the REST API (so it never triggers via WebSockets).\n```elixir\nSimpleClient.trigger(\"private-channel\", \"event\", %{message: \"message\"}, force_api: true)\n```\n\n#### channels/0\nReturns map of all the active channels which are being used in your Pusher application.\nCan contain informations about subscribed users.\n```elixir\nSimpleClient.channels()\n%{\"channels\" =\u003e %{\"public-channel\" =\u003e %{}, \"private-channel\" =\u003e %{}}}\n```\n\n#### subscribed_channels/0\nReturns list of all the subscribed channels for current instance.\n```elixir\nSimpleClient.subscribed_channels()\n[\"private-channel\"]\n```\n\n#### presence/0\nReturns information about all the users subscribed to a presence channel.\n```elixir\nSimpleClient.presence()\n%Pushest.Data.Presence{\n  count: 2,\n  hash: %{\"1\" =\u003e %{\"name\" =\u003e \"Tomas\"}, \"2\" =\u003e %{\"name\" =\u003e \"Jose\"}},\n  ids: [\"1\", \"2\"],\n  me: %{user_id: \"2\", user_info: %{name: \"Jose\"}}\n}\n```\n\n#### unsubscribe/1\nUnsubscribes from given channel\n```elixir\nSimpleClient.unsubscribe(\"public-channel\")\n```\n\n### Overridable functions\nThese functions are meant to be overridden in a module using Pushest\n#### handle_event/2\nCallback being triggered when there is a WebSocket event on a subscribed channel.\n```elixir\ndefmodule MyApp.MyModule\n  use Pushest, otp_app: :my_app\n\n  def handle_event({:ok, \"my-channel\", \"my-event\"}, frame) do\n    IO.inspect frame\n  end\nend\n```\n\n#### init_channels/0\nSubscribes to given list of channels right after application startup.\nEach element has to be a keyword list in exact format of: `[name: String.t(), user_data: map]`\n```elixir\ndefmodule MyApp.MyModule\n  use Pushest, otp_app: :my_app\n\n  def init_channels do\n    [\n      [name: \"public-init-channel\", user_data: %{}],\n      [name: \"private-init-channel\", user_data: %{}],\n      [name: \"presence-init-channel\", user_data: %{user_id: 123}],\n    ]\n  end\nend\n```\n\n#### `frame` example\n`frame` is a `Pushest.Socket.Data.Frame` or `Pushest.Api.Data.Frame` struct with data payload as a map. \n```elixir\n%Pushest.Data.Frame{\n  channel: \"private-channel\",\n  data: %{\"name\" =\u003e \"John\", \"message\" =\u003e \"Hello\"},\n  event: \"second-event\"\n}\n```\n\n## Installation\n\nThe package can be installed by adding `pushest` to your list of dependencies in `mix.exs`:\n\n```elixir\ndef deps do\n  [\n    {:pushest, \"~\u003e 0.2.2\"}\n  ]\nend\n```\n\n## Documentation\n\nDocumentation can be be found at [https://hexdocs.pm/pushest](https://hexdocs.pm/pushest).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstepnivlk%2Fpushest","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstepnivlk%2Fpushest","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstepnivlk%2Fpushest/lists"}