This module is a wrapper around the Socket, Channel and Presence modules. It handles all the low level stuff with a simple, but extensive API. It automates a few processes, and generally simplifies working with Phoenix WebSockets.
Once you have installed the package, and followed the simple setup instructions here, configuring this module is as simple as this:
import Phoenix
import Ports.Phoenix as Ports
-- Add the Phoenix Model to your Model
type alias Model =
{ phoenix : Phoenix.Model
...
}
-- Initialize the Phoenix Model
init : Model
init =
{ phoenix = Phoenix.init Ports.config
...
}
-- Add a Phoenix Msg
type Msg
= PhoenixMsg Phoenix.Msg
| ...
-- Handle Phoenix Msgs
update : Msg -> Model -> (Model Cmd Msg)
update msg model =
case msg of
PhoenixMsg subMsg ->
let
(phoenix, phoenixCmd, phoenixMsg) =
Phoenix.update subMsg model.phoenix
in
case phoenixMsg of
...
_ ->
({ model | phoenix = phoenix }
, Cmd.map PhoenixMsg phoenixCmd
)
_ ->
(model, Cmd.none)
-- Subscribe to receive Phoenix Msgs
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.map PhoenixMsg <|
Phoenix.subscriptions model.phoenixA type alias representing the ports that are needed to communicate with JS.
This is for reference only, you won't need this if you copy
this file
into your src.
Initialize the Model by providing the ports that enable
communication with JS.
The easiest way to provide the ports is to copy
this file
into your src, and then use its config function as follows:
import Phoenix
import Ports.Phoenix as Ports
init : Model
init =
{ phoenix = Phoenix.init Ports.config
...
}The Msg type that you pass into the update function.
This is an opaque type, for pattern matching see PhoenixMsg.
A type alias representing the Channel topic id, for example
"topic:subTopic".
A type alias representing an event that is sent to, or received from, a Channel.
A type alias representing data that is sent to, or received from, a Channel.
A type alias representing the original payload that was sent with a push.
A type alias representing a Presence on a Channel.
id - The id used to identify the Presence map in the
Presence.track/3
Elixir function. The recommended approach is to use the users' id.
metas- A list of metadata as stored in the
Presence.track/3
function.
user - The user data that is pulled from the DB and stored on the
Presence in the
fetch/2
Elixir callback function. This is the recommended approach for storing user
data on the Presence. If
fetch/2 is
not being used then user will be equal to
Json.Encode.null.
presence - The whole Presence map. This provides a way to access any
additional data that is stored on the Presence.
-- MyAppWeb.MyChannel.ex
def handle_info(:after_join, socket) do
{:ok, _} = Presence.track(socket, socket.assigns.user_id, %{
online_at: System.os_time(:millisecond)
})
push(socket, "presence_state", Presence.list(socket))
{:noreply, socket}
end
-- MyAppWeb.Presence.ex
defmodule MyAppWeb.Presence do
use Phoenix.Presence,
otp_app: :my_app,
pubsub_server: MyApp.PubSub
def fetch(_topic, presences) do
query =
from u in User,
where: u.id in ^Map.keys(presences),
select: {u.id, u}
users = query |> Repo.all() |> Enum.into(%{})
for {key, %{metas: metas}} <- presences, into: %{} do
{key, %{metas: metas, user: users[key]}}
end
end
endAn InternalError should never happen, but if it does, it is because the
JS is out of sync with this package.
If you ever receive this message, please raise an issue.
Pattern match on these in your update function.
To handle events that are pushed or broadcast from an Elixir Channel you
should pattern match on ChannelEvent.
import Phoenix
type alias Model =
{ phoenix : Phoenix.Model
...
}
type Msg
= PhoenixMsg Phoenix.Msg
| ...
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
PhoenixMsg subMsg ->
let
(phoenix, phoenixCmd, phoenixMsg) =
Phoenix.update subMsg model.phoenix
in
case phoenixMsg of
...
_ ->
( { model | phoenix = phoenix }
, Cmd.map PhoenixMsg phoenixCmd
)
_ ->
(model, Cmd.none)Helper function to use with update in order to:
phoenix field on the ModelCmd Phoenix.Msg generated by Phoenix.update to a Cmd Msg.import Phoenix
type alias Model =
{ phoenix : Phoenix.Model
...
}
type Msg
= PhoenixMsg Phoenix.Msg
| ...
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
PhoenixMsg subMsg ->
let
(newModel, cmd, phoenixMsg) =
Phoenix.update subMsg model.phoenix
|> Phoenix.updateWith PhoenixMsg model
in
case phoenixMsg of
...
_ ->
(newModel, cmd)
_ ->
(model, Cmd.none)Note: To use this function, Phoenix.Model needs to be stored on field phoenix on
the Model.
Map the Phoenix Model and Cmd Msg that is generated by
the update function onto your model and msg.
import Phoenix
type alias Model =
{ websocket : Phoenix.Model
...
}
type Msg
= PhoenixMsg Phoenix.Msg
| ...
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
PhoenixMsg subMsg ->
let
(newModel, cmd, phoenixMsg) =
Phoenix.update subMsg model.websocket
|> Phoenix.map (\p m -> { m | websocket = p }) model PhoenixMsg
in
...Map your msg onto the Phoenix Cmd Msg that is generated by the
update function. This is useful if you want to work with the Phoenix
Model some more before updating your own Model.
import Phoenix
type alias Model =
{ websocket : Phoenix.Model
...
}
type Msg
= PhoenixMsg Phoenix.Msg
| ...
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
PhoenixMsg subMsg ->
let
(phoenix, cmd, phoenixMsg) =
Phoenix.update subMsg model.websocket
|> Phoenix.mapMsg PhoenixMsg
in
...When pushing an event to a Channel, opening the Socket, and joining the Channel is handled automatically. Pushes will be queued until the Channel has been joined, at which point, any queued pushes will be sent in a batch.
See Connecting to the Socket and Joining a Channel for more details on handling connecting and joining manually.
If the Socket is open and the Channel already joined, the push will be sent immediately.
The retry strategy to use if a push times out.
Drop - Drop the push and don't try again. This is the default if no
strategy is set.
Every second - The number of seconds to wait between retries.
Backoff [List seconds] (Maybe max) - A backoff strategy enabling you to increase
the delay between retries. When the list has been exhausted, max will be
used for each subsequent attempt, if max is Nothing, the push will then
be dropped, which is useful if you want to limit the number of retries.
Backoff [ 1, 5, 10, 20 ] (Just 30)A type alias representing the config for pushing a message to a Channel.
topic - The Channel topic to send the push to.
event - The event to send to the Channel.
payload - The data to send with the push. If you don't need to send any
data, set this to
Json.Encode.null.
timeout - Optional timeout in milliseconds to set on the push request.
retryStrategy - The retry strategy to use if the push times out.
ref - Optional reference that can later be used to identify the push.
This is useful when using functions that need to find the push in order to
do their thing, such as dropPush or
pushTimeoutCountdown.
A helper function for creating a PushConfig.
import Phoenix exposing (pushConfig)
{ pushConfig
| topic = "topic:subTopic"
, event = "hello"
}Push a message to a Channel.
import Json.Encode as JE
import Phoenix exposing (pushConfig)
Phoenix.push
{ pushConfig
| topic = "topic:subTopic"
, event = "event1"
, payload =
JE.object
[("foo", JE.string "bar")]
}
model.phoenixReceive Msgs from the Socket, Channels and Phoenix Presence.
import Phoenix
type alias Model =
{ phoenix : Phoenix.Model
...
}
type Msg
= PhoenixMsg Phoenix.Msg
| ...
subscriptions : Model -> Sub Msg
subscriptions model =
Sub.map PhoenixMsg <|
Phoenix.subscriptions model.phoenixAdd some ConnectOptions to set on the Socket when it is created.
Note: This will overwrite any like for like ConnectOptions that have already been set.
import Phoenix
import Phoenix.Socket exposing (ConnectOption(..))
import Ports.Phoenix as Ports
type alias Model =
{ phoenix : Phoenix.Model
...
}
init : Model
init =
{ phoenix =
Phoenix.init Ports.config
|> Phoenix.addConnectOptions
[ Timeout 7000
, HeartbeatIntervalMillis 2000
]
|> Phoenix.addConnectOptions
[ Timeout 5000 ]
...
}
-- List ConnectOption == [ Timeout 5000, HeartbeatIntervalMillis 2000 ]Provide the ConnectOptions to set on the Socket when it is created.
Note: This will replace all previously set ConnectOptions.
import Phoenix
import Phoenix.Socket exposing (ConnectOption(..))
import Ports.Phoenix as Ports
type alias Model =
{ phoenix : Phoenix.Model
...
}
init : Model
init =
{ phoenix =
Phoenix.init Ports.config
|> Phoenix.addConnectOptions
[ HeartbeatIntervalMillis 2000 ]
|> Phoenix.setConnectOptions
[ Timeout 7000 ]
|> Phoenix.setConnectOptions
[ Timeout 5000 ]
...
}
-- List ConnectOption == [ Timeout 5000 ]Provide some params to send to the connect function at the Elixir end.
import Json.Encode as JE
import Phoenix
type alias Model =
{ phoenix : Phoenix.Model
...
}
init : Model
init =
{ phoenix =
Phoenix.init Ports.config
|> Phoenix.setConnectParams
( JE.object
[ ("username", JE.string "username")
, ("password", JE.string "password")
]
)
...
}Disconnect the Socket, maybe providing a status code for the closure.
ConnectOptions and configs etc will remain to make it simpler to connect again. If you want to reset everything use the disconnectAndReset function instead.
Disconnect the Socket, maybe providing a status code for the closure.
This will reset all of the internal model, so information relating to ConnectOptions and configs etc will also be reset.
The only information that will remain is the PortConfig.
Join a Channel referenced by the Topic.
Connecting to the Socket is automatic if it has not already been done.
If the Socket is not open, the join will be queued, and the Socket will try
to connect. Once the Socket is open, any queued joins will be attempted.
If the Socket is already open, the join will be attempted immediately.
Join a List of Channels.
The joins will be batched together, the order in which the requests
will reach their respective Channels at the Elixir end is undetermined.
A type alias representing the optional config to use when joining a Channel.
topic - The channel topic id, for example: "topic:subTopic".
payload - Data to be sent to the Channel when joining. If no data is
required, set this to
Json.Encode.null.
Defaults to
Json.Encode.null.
events - A list of events to receive from the Channel. Defaults to [].
timeout - Optional timeout, in ms, before retrying to join if the previous
attempt failed. Defaults to Nothing.
If a JoinConfig is not set prior to joining a Channel, the defaults will be used.
A helper function for creating a JoinConfig.
import Phoenix exposing (joinConfig)
{ joinConfig
| topic = "topic:subTopic"
, events = [ "event1", "event2" ]
}Set a JoinConfig to be used when joining a Channel.
import Phoenix exposing (joinConfig)
import Ports.Phoenix as Port
type alias Model =
{ phoenix : Phoenix.Model
...
}
init : Model
init =
{ phoenix =
Phoenix.init Port.config
|> Phoenix.setJoinConfig
{ joinConfig
| topic = "topic:subTopic"
, events = [ "event1", "event2" ]
}
...
}A type alias representing the optional config to use when leaving a Channel.
topic - The channel topic id, for example: "topic:subTopic".
timeout - Optional timeout, in ms, before retrying to leave if the
previous attempt failed. Defaults to Nothing.
If a LeaveConfig is not set prior to leaving a Channel, the defaults will be used.
Set a LeaveConfig to be used when leaving a Channel.
import Phoenix
import Ports.Phoenix as Port
type alias Model =
{ phoenix : Phoenix.Model
...
}
init : Model
init =
{ phoenix =
Phoenix.init Port.config
|> Phoenix.setLeaveConfig
{ topic = "topic:subTopic"
, timeout = Just 5000
}
...
}Setting up incoming events to receive on a Channel can be done when setting a JoinConfig, but if it is necessary to switch events on and off intermittently, then the following functions are available.
The current state of the Socket.
The current state of the Socket as a String.
Whether the Socket is connected or not.
The current connection state of the Socket as a String.
The reason the Socket disconnected.
The endpoint URL for the Socket.
Channels that are queued waiting to join.
Determine if a Channel is in the queue to join.
Channels that have joined successfully.
Determine if a Channel has joined successfully.
Split a topic into it's component parts.
Pushes that are queued and waiting for their Channel to join before being sent.
Retrieve a list of pushes, by Topic, that are queued and waiting for their Channel to join before being sent.
Determine if a Push is in the queue to be sent when its' Channel joins.
pushQueued
(\push -> push.ref == "custom ref")
model.phoenixDetermine if a Push has been sent and is on it's way to the Elixir Channel.
pushInFlight
(\push -> push.event == "custom_event")
model.phoenixDetermine if a Push is waiting to be received by its' Channel.
This means the push could be in any off the following states:
in a queue waiting for its' Channel to be joined before being sent
in flight and on its way to the Channel
timed out and waiting to be tried again according to its' RetryStrategy
This is useful if you just need to determine if a push is being actioned. Maybe you need to disable/hide a button until the PushOk message has been received?
pushWaiting
(\push -> push.event == "custom_event")
model.phoenixPushes that have timed out and are waiting to be sent again in accordance with their RetryStrategy.
Pushes with a RetryStrategy of Drop, won't make it here.
Determine if a Push has timed out and will be tried again in accordance with it's RetryStrategy.
pushTimedOut
(\push -> push.ref == "custom ref")
model.phoenixCancel a timed out Push.
dropTimeoutPush
(\push -> push.topic == "topic:subTopic")
model.phoenixThis will only work after a push has timed out and before it is re-tried.
Maybe get the number of seconds until a push is retried.
This is useful if you want to show a countdown timer to your users.
Cancel a Push.
This will cancel pushes that are queued to be sent when their Channel joins. It will also prevent pushes that timeout from being re-tried.
A list of Presences on the Channel referenced by Topic.
A list of Presence diffs on the Channel referenced by Topic.
A list of Presences that have joined the Channel referenced by Topic.
A list of Presences that have left the Channel referenced by Topic.
Maybe the last Presence to join the Channel referenced by Topic.
Maybe the last Presence to leave the Channel referenced by Topic.
Batch a list of functions together.
import Phoenix exposing (pushConfig)
Phoenix.batch
[ Phoenix.join "topic:subTopic3"
, Phoenix.leave "topic:subTopic2"
, Phoenix.push
{ pushConfig
| topic = "topic:subTopic1"
, event = "hello"
}
]
model.phoenixBatch a list of parameters onto their functions.
import Phoenix
Phoenix.batchWithParams
[ (Phoenix.join, [ "topic:subTopic1", "topic:subTopic2" ])
, (Phoenix.leave, [ "topic:subTopic3", "topic:subTopic4" ])
, (Phoenix.push, [ pushConfig1, pushConfig2, pushConfig3 ])
]
model.phoenixHere you can log data to the console, and activate and deactive the socket's
logger, but be warned, there is no safeguard when you compile such as you
get when you use Debug.log. Be sure to deactive the logging before you deploy
to production.
However, the ability to easily toggle logging on and off leads to a possible use case where, in a deployed production environment, an admin is able to see all the logging, while regular users do not.
Log some data to the console.
import Json.Encode as JE
log "info" "foo"
(JE.object
[ ( "bar", JE.string "foo bar" ) ]
)
model.phoenix
-- info: foo {bar: "foo bar"}In order to receive any output in the console, you first need to activate the
socket's logger. There are two ways to do this. You can use the
startLogging function, or you can set the Logger True
ConnectOption.
import Phoenix
import Phoenix.Socket exposing (ConnectOption(..))
import Ports.Phoenix as Ports
init : Model
init =
{ phoenix =
Phoenix.init Ports.config
|> Phoenix.setConnectOptions
[ Logger True ]
...
}Activate the socket's logger function. This will log all messages that the socket sends and receives.
Deactivate the socket's logger function.
The model that carries the internal state.
This is an opaque type, so use the provided API to interact with it.