View Source Rejoin on Reconnect
Slipstream provides rejoin/3
and reconnect/1
to provide out-of-the-box
retry mechanisms for re-establishing a client's functions after network
outages.
For clients that
- use the default implementation of
Slipstream.handle_disconnect/2
- call
Slipstream.connect/2
orSlipstream.connect!/2
inSlipstream.init/1
- and call
Slipstream.join/3
inSlipstream.handle_connect/1
These retry mechanisms work without any extra effort. But for clients which
dynamically join topics, you will have to take re-joins into account after a
disconnect, since that third condition about Slipstream.join/3
ing may not
be true.
Consider a client written like so:
defmodule MyClient do
use Slipstream
def join(topic) do
GenServer.cast(__MODULE__, {:join, topic})
end
def start_link(config) do
Slipstream.start_link(__MODULE__, config, name: __MODULE__)
end
@impl Slipstream
def init(config) do
{:ok, connect!(config)}
end
@impl Slipstream
def handle_cast({:join, topic}, socket) do
{:noreply, join(socket, topic)}
end
end
This client uses the default implementation of
Slipstream.handle_disconnect/2
, but does not join any topics in its
Slipstream.handle_connect/1
callback (since it uses the default
implementation). It doesn't know the topics it wishes to join ahead of time,
so we say that this client joins topics dynamically.
Dynamic clients like this one will not automatically re-join after a
disconnect (however they will re-connect automatically due to the usage of the
default Slipstream.handle_disconnect/2
callback).
Tutorial
In this tutorial, we will start with the above client and refactor it so that it will gracefully re-join all topics when it reconnects after a disconnect.
We will start by modifying the Slipstream.init/1
callback to store a list
of topics in the socket assigns using Slipstream.Socket.assign/3
(2b0bd58
)
@impl Slipstream
def init(config) do
socket =
config
|> connect!()
|> assign(:topics, [])
{:ok, socket}
end
And every time the client is told to join a topic with MyClient.join/1
, we
want to update that socket.assigns.topics
key to store the new topic. We
do that in the Slipstream.handle_cast/2
callback with
Slipstream.Socket.update/3
(70e802f
)
@impl Slipstream
def handle_cast({:join, new_topic}, socket) do
socket =
socket
|> update(:topics, fn existing_topics -> [new_topic | existing_topics] end)
|> join(new_topic)
{:noreply, socket}
end
So now we have in socket.assigns.topics
a list of all topics that we have
attempted to join. (Curious how to store only the topics we have successfully
joined? That is left as an exercise to the reader, with the solution written as
an HTML comment at the bottom of this file.)
Now we must implement a mechanism to re-join after a disconnect. More
accurately, we must implement a mechanism to re-join after a re-connection.
When a client is re-connected after a disconnect, the
Slipstream.handle_connect/1
callback is invoked again (it is first
invoked upon the first successful connection to the remote websocket server).
That's the perfect place to put our re-join mechanism
(70e802f
)
@impl Slipstream
def handle_connect(socket) do
socket =
socket.assigns.topics
|> Enum.reduce(socket, fn topic, socket ->
case rejoin(socket, topic) do
{:ok, socket} -> socket
{:error, _reason} -> socket
end
end)
{:ok, socket}
end
Whenever we connect, be it the first connect or a re-connection, we will
attempt to Slipstream.rejoin/3
all the topics in socket.assigns.topic
.
On the initial connect this list is empty, so we keep the dynamic nature of
only joining topics on request (via GenServer.cast/2
).
Think that this should be the default behavior of a reconnection? Advocate for this feature being built-in behavior in #18.