README.md

# phoenix_gen_socket_client

![CI](https://github.com/alboratech/phoenix_gen_socket_client/workflows/CI/badge.svg)
[![hex.pm](https://img.shields.io/hexpm/v/phx_gen_socket_client.svg?style=flat-square)](https://hex.pm/packages/phoenix_gen_socket_client)
[![hexdocs.pm](https://img.shields.io/badge/docs-latest-green.svg?style=flat-square)](https://hexdocs.pm/phx_gen_socket_client/)

> **Fork of the [phoenix_gen_socket_client][phoenix_gen_socket_client] library.**

This library implements an Elixir client for Phoenix Channels protocol 2.x.
The client is implemented as a behaviour, which allows a lot of flexibility.
For an alternative approach, you may also want to check
[this project](https://github.com/mobileoverlord/phoenix_channel_client).

__NOTE__: From version 2.0 onwards this library only supports version 2.0 of
the channels protocol (used on Phoenix 1.3 or later). If you need to use the
version 1.0 of the protocol, you need to use
[the older version of this library](https://hex.pm/packages/phoenix_gen_socket_client/1.2.0).

[phoenix_gen_socket_client]: https://github.com/Aircloak/phoenix_gen_socket_client


## Installation

You need to add the project as a dependency to your `mix.exs`:

```elixir

def project do
  [
    deps: [
      {:phoenix_gen_socket_client, "~> 3.3.0", hex: :phx_gen_socket_client}
      # ...
    ],
    # ...
  ]
end
```

You also need to add the transport (e.g. a websocket client), and serializer
(e.g. JSON) as dependencies. Out of the box, the adapter for
[sanmiguel/websocket_client][websocket_client], and support for
[Jason](https://hex.pm/packages/jason) is provided but you still
need to include the libraries yourself:

```elixir
def project do
  [
    deps: [
      {:websocket_client, "~> 1.2"},
      {:jason, "~> 1.1"}

      # ...
    ],
    # ...
  ]
end
```

[websocket_client]: https://github.com/sanmiguel/websocket_client

## Usage

This library is designed for flexibility, so the usage is a bit more involved.
In general, you need to:

1. Implement a callback module for the `Phoenix.Channels.GenSocketClient`
   behaviour.
2. Start the socket process somewhere in your supervision tree.
3. Interact with the server from callback functions which are running in the
   socket process.

A simple demo is available [here][demo]. Here, we'll present some general ideas.
For more details, refer to the [documentation][documentation].

__Note__: In the subsequent code snippets we assume that
`Phoenix.Channels.GenSocketClient` is aliased, so we use `GenSocketClient`.

[demo]: https://github.com/alboratech/phoenix_gen_socket_client/tree/master/example
[documentation]: https://hexdocs.pm/phx_gen_socket_client/Phoenix.Channels.GenSocketClient.html

### Starting the socket process

Starting the process works similar to other behaviours, such as `GenServer`:

```elixir
GenSocketClient.start_link(
  callback_module,
  transport_module,
  arbitrary_argument,
  socket_opts, # defaults to []
  gen_server_opts # defaults to []
)
```

For `transport_module` you can pass any module which implements the
`Phoenix.Channels.GenSocketClient.Transport` behaviour. Out of the box, you have
the module `Phoenix.Channels.GenSocketClient.Transport.WebSocketClient`
available.

The code above will start another process where the `init/1` function of the
`callback_module` is invoked. This function needs to provide the initial state
and the socket url. The tuple also determines whether the connection will be
immediately established or not.

The socket url must also include the transport suffix. For example, if in the
server socket you have declared socket with `socket "/my_socket", ...`, then
the url for the websocket transport would be
`ws://server_url/my_socket/websocket`.


### Connection life-cycle

If `init/1` returns `{:connect, url, query_params, initial_state}` the socket
process will try to connect to the server. The connection is established in a
separate process which we call the transport process. This process is the
immediate child of the socket process. As a consequence, all communication takes
place concurrently to the socket process. If you handle Erlang messages in the
socket process you may need to keep track of whether you're connected or not.

The establishing of the connection is done asynchronously. The
`handle_connected/2` callback is invoked after the connection is established.
The `handle_disconnected/2` callback is invoked if establishing the connection
fails or an existing connection is lost.

If the connection is not established (or dropped), you can reconnect from
`handle_*` functions by returning `{:connect, state}` tuple. In this case the
workflow is the same as when returning the `:connect` tuple from the `init/1`
callback.

You may also decide to defer connecting to a later point in time by returning
`{:noconnect, url, query_params, state}` from the `init/1` callback. To later
establish a connection you need to send some message to the socket process,
and handle that message in `handle_info` by returning the `{:connect, state}`
tuple.

You may also return `{:connect, url, query_params, state}` from a `handle_info`
callback to connect (or reconnect) the socket to a different location or with
different params than it started with.


Though somewhat elaborate, this approach has following benefits:

1. The socket process starts immediately without waiting for the connection to
   be established.
2. The socket process can live in the supervision tree even if the connection
   is not established.
3. Implementing reconnection logic is very flexible. You can trigger a reconnect
   directly from `handle_disconnected/2`, or send yourself a delayed message.
   You can also easily accumulate outgoing messages until you reconnect, or you
   can attempt a finite number of reconnects and then give up.


### Sending messages over a socket

Once you're connected to the socket, you can issue messages. Most `handle_*`
callbacks receive a `transport` argument. You can pass this argument to
functions such as `GenSocketClient.join/3`, `GenSocketClient.leave/3`,
`GenSocketClient.push/4` to send messages over a connected socket. If you're
disconnected when calling these functions, they will return an error. Otherwise
they return a successful response.

Of course, there's no guarantees that the message has arrived to the server,
since a netsplit can happen at any point in time.


### Channel life-cycle

To join a topic, you can use `GenSocketClient.join/3`. Depending on the outcome,
`handle_joined/4` or `handle_join_error/4` callback is invoked. If the server
leaves the channel at some later point, `handle_channel_closed/4` callback will
be invoked. Just like with socket connections, this gives you a lot of
flexibility if you want to implement the rejoin logic. On socket disconnect,
all channels are closed, but `handle_channel_closed/4` is not invoked.

You can of course join multiple topics on the same socket. Unlike in Phoenix,
all messages are handled in the socket process. Besides the socket and the
transport process, there are no additional processes created. Such decisions
are deliberately left out of the `GenSocketClient` to support more flexibility.


### Communicating on a channel

To send a message on the connected channel, you can use
`GenSocketClient.push/4`. If the socket has not joined the topic, an error is
returned. On success, the message reference (aka _ref_) is returned.

If the server-side channel replies directly (using the `{:reply, ...}`), the
`handle_reply/5` callback is invoked with the matching ref. Refs are unique
only per channel-session. Two channels can use the same refs. Also, refs can be
reused after you rejoin a topic.

If the server sends an asynchronous message (i.e. not a direct reply), the
`handle_message/5` callback is invoked.


## Copyright and License

Copyright (c) 2020 Aircloak

The source code is licensed under the [MIT License](https://github.com/alboratech/phoenix_gen_socket_client/blob/master/LICENSE.md).