defmodule RealtimeClient do
@moduledoc """
Client library to work with [Realtime](https://github.com/supabase/realtime).
It's mostly a wrapper around [Phoenix Client](https://github.com/mobileoverlord/phoenix_client).
## Getting started
First you have to create a client Socket:
options = [
url: "ws://realtime-server:4000/socket/websocket",
]
{:ok, socket} = RealtimeClient.socket(options)
Once you have a connected socket, you can subscribe to topics:
{:ok, channel} = RealtimeClient.subscribe(socket, "realtime:*")
You can also subscribe to a specific channel (row level changes):
{:ok, channel} = RealtimeClient.subscribe(socket, "realtime:public:users:id=eq.42")
Consuming events is done with `handle_info` callbacks:
alias PhoenixClient.Message
# handle `INSERT` events
def handle_info(%Message{event: "INSERT", payload: %{"record" => record}} = msg, state) do
# do something with record
{:noreply, state}
end
# handle `DELETE` events
def handle_info(%Message{event: "DELETE", payload: %{"record" => record}} = msg, state) do
IO.inspect(record, label: "DELETE")
{:noreply, state}
end
# match all cases not handled above
def handle_info(%Message{} = msg, state) do
{:noreply, state}
end
## Configuration
Socket endpoint and parameters can also be configured:
config :realtime_client,
endpoint: "ws://realtime-server:4000/socket/websocket",
apikey: "eyJhbGciOiJIUzI1MiIsInR5cCI6IkpXVCJ9.eyJJc3N1ZXIiOiJJc3N1ZXIifQ.LNcM66Tt3ejSf0fHJ-I8yh8Hgfmvh8I_CXyBIOU8S6c"
Creating the socket can then be done with:
{:ok, socket} = RealtimeClient.socket()
"""
alias PhoenixClient.{Socket, Channel}
@doc false
def init(opts) do
opts = init_opts(opts)
Socket.init(opts)
end
@doc false
def start_link(opts) do
name = Keyword.get(opts, :name, Realtime.Socket)
opts = init_opts(opts)
Socket.start_link(opts, name: name)
end
@doc false
def child_spec(opts) do
socket_opts = init_opts(opts)
Socket.child_spec({socket_opts, name: Realtime.Socket})
end
def subscribe(topic) do
case Channel.join(Realtime.Socket, topic) do
{:ok, _, channel} -> {:ok, channel}
error -> error
end
end
@doc """
Subscribes to a topic through given socket.
In cases where the socket is not connected (yet), the function is
retried (see `subscribe/4`).
* `socket` - The name of pid of the client socket
* `topic` - The topic to subscribe to
"""
def subscribe(socket, topic) do
subscribe(socket, topic, 3)
end
def subscribe(socket, topic, retires, error \\ nil)
def subscribe(_socket, _topic, 0, error) do
error
end
def subscribe(socket, topic, retries, _error) do
case Channel.join(socket, topic) do
{:ok, _, channel} ->
{:ok, channel}
error ->
Process.sleep(100)
subscribe(socket, topic, retries - 1, error)
end
end
@doc """
Creates a new client socket.
* `opts` - The optional list of options. See below.
## Options
* `url` - the url of the websocket to connect to
* `params` - the params to send to the websocket, e.g. to pass an api key
"""
def socket(opts \\ []) do
init_opts(opts)
|> Socket.start_link()
end
defp init_opts(opts) do
url =
Keyword.get_lazy(opts, :url, fn -> Application.fetch_env!(:realtime_client, :endpoint) end)
params =
case Keyword.get(opts, :params, %{}) |> Map.get(:apikey) do
nil ->
apikey = Application.fetch_env!(:realtime_client, :apikey)
%{apikey: apikey}
_ ->
opts[:params]
end
[url: url, params: params]
end
end