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.phoenix
The model that carries the internal state.
This is an opaque type, so use the provided API to interact with it.
{ phoenixSend : { msg : String
, payload : Json.Encode.Value } -> Platform.Cmd.Cmd Msg
, socketReceiver : ({ msg : String
, payload : Json.Encode.Value } -> Msg) -> Platform.Sub.Sub Msg
, channelReceiver : ({ topic : String
, msg : String
, payload : Json.Encode.Value } -> Msg) -> Platform.Sub.Sub Msg
, presenceReceiver : ({ topic : String
, msg : String
, payload : Json.Encode.Value } -> Msg) -> Platform.Sub.Sub Msg
}
A 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
.
init : PortConfig -> Model
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.
String
A type alias representing the Channel topic id, for example
"topic:subTopic"
.
String
A type alias representing an event that is sent to, or received from, a Channel.
Json.Encode.Value
A type alias representing data that is sent to, or received from, a Channel.
Json.Encode.Value
A type alias representing the original payload that was sent with a push.
Maybe String
A type alias representing the ref
set on a push.
{ id : String
, metas : List Json.Encode.Value
, user : Json.Encode.Value
, presence : Json.Encode.Value
}
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
end
{ joins : List Presence
, leaves : List Presence
}
An 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 push
ed or broadcast
from an Elixir Channel you
should pattern match on ChannelEvent
.
update : Msg -> Model -> ( Model, Platform.Cmd.Cmd Msg, PhoenixMsg )
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)
updateWith : (Msg -> msg) -> { model | phoenix : Model } -> ( Model, Platform.Cmd.Cmd Msg, PhoenixMsg ) -> ( { model | phoenix : Model }, Platform.Cmd.Cmd msg, PhoenixMsg )
Helper function to use with update in order to:
phoenix
field on the Model
Cmd 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 : (Model -> model -> model) -> model -> (Msg -> msg) -> ( Model, Platform.Cmd.Cmd Msg, PhoenixMsg ) -> ( model, Platform.Cmd.Cmd msg, PhoenixMsg )
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
...
mapMsg : (Msg -> msg) -> ( Model, Platform.Cmd.Cmd Msg, PhoenixMsg ) -> ( Model, Platform.Cmd.Cmd msg, PhoenixMsg )
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)
{ topic : String
, event : String
, payload : Json.Encode.Value
, timeout : Maybe Basics.Int
, retryStrategy : RetryStrategy
, ref : Maybe String
}
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.
pushConfig : PushConfig
A helper function for creating a PushConfig.
import Phoenix exposing (pushConfig)
{ pushConfig
| topic = "topic:subTopic"
, event = "hello"
}
push : PushConfig -> Model -> ( Model, Platform.Cmd.Cmd Msg )
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.phoenix
subscriptions : Model -> Platform.Sub.Sub Msg
Receive Msg
s 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.phoenix
Connecting to the Socket is automatic on the first push to a Channel, and also when a join is attempted. However, if it is necessary to connect before hand, the connect function is available.
connect : Model -> ( Model, Platform.Cmd.Cmd Msg )
Connect to the Socket.
addConnectOptions : List Socket.ConnectOption -> Model -> Model
Add 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 ]
setConnectOptions : List Socket.ConnectOption -> Model -> Model
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 ]
setConnectParams : Json.Encode.Value -> Model -> Model
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 : Maybe Basics.Int -> Model -> ( Model, Platform.Cmd.Cmd Msg )
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.
disconnectAndReset : Maybe Basics.Int -> Model -> ( Model, Platform.Cmd.Cmd Msg )
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.
Joining a Channel is automatic on the first push to the Channel. However, if it is necessary to join before hand, the join function is available.
join : Topic -> Model -> ( Model, Platform.Cmd.Cmd Msg )
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 join
s will be attempted.
If the Socket is already open, the join
will be attempted immediately.
joinAll : List Topic -> Model -> ( Model, Platform.Cmd.Cmd Msg )
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.
{ topic : String
, payload : Json.Encode.Value
, events : List String
, timeout : Maybe Basics.Int
}
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.
joinConfig : JoinConfig
A helper function for creating a JoinConfig.
import Phoenix exposing (joinConfig)
{ joinConfig
| topic = "topic:subTopic"
, events = [ "event1", "event2" ]
}
setJoinConfig : JoinConfig -> Model -> Model
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" ]
}
...
}
leave : Topic -> Model -> ( Model, Platform.Cmd.Cmd Msg )
Leave a Channel referenced by the Topic.
leaveAll : Model -> ( Model, Platform.Cmd.Cmd Msg )
Leave all joined Channels.
{ topic : Topic
, timeout : Maybe Basics.Int
}
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.
setLeaveConfig : LeaveConfig -> Model -> Model
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.
addEvent : Topic -> Event -> Model -> Platform.Cmd.Cmd Msg
Add the Event you want to receive from the Channel identified by Topic.
addEvents : Topic -> List Event -> Model -> Platform.Cmd.Cmd Msg
Add the Events you want to receive from the Channel identified by Topic.
dropEvent : Topic -> Event -> Model -> Platform.Cmd.Cmd Msg
Remove an Event you no longer want to receive from the Channel identified by Topic.
dropEvents : Topic -> List Event -> Model -> Platform.Cmd.Cmd Msg
Remove Events you no longer want to receive from the Channel identified by Topic.
socketState : Model -> SocketState
The current state of the Socket.
socketStateToString : Model -> String
The current state of the Socket as a String.
isConnected : Model -> Basics.Bool
Whether the Socket is connected or not.
connectionState : Model -> String
The current connection state of the Socket as a String.
disconnectReason : Model -> Maybe String
The reason the Socket disconnected.
endPointURL : Model -> String
The endpoint URL for the Socket.
protocol : Model -> String
The protocol being used by the Socket.
queuedChannels : Model -> List String
Channels that are queued waiting to join.
channelQueued : Topic -> Model -> Basics.Bool
Determine if a Channel is in the queue to join.
joinedChannels : Model -> List String
Channels that have joined successfully.
channelJoined : Topic -> Model -> Basics.Bool
Determine if a Channel has joined successfully.
topicParts : Topic -> List String
Split a topic into it's component parts.
allQueuedPushes : Model -> Dict Topic (List PushConfig)
Pushes that are queued and waiting for their Channel to join before being sent.
queuedPushes : Topic -> Model -> List PushConfig
Retrieve a list of pushes, by Topic, that are queued and waiting for their Channel to join before being sent.
pushQueued : (PushConfig -> Basics.Bool) -> Model -> Basics.Bool
Determine if a Push is in the queue to be sent when its' Channel joins.
pushQueued
(\push -> push.ref == "custom ref")
model.phoenix
dropQueuedPush : (PushConfig -> Basics.Bool) -> Model -> Model
Cancel a queued Push that is waiting for its' Channel to join.
dropQueuedPush
(\push -> push.topic == "topic:subTopic")
model.phoenix
pushInFlight : (PushConfig -> Basics.Bool) -> Model -> Basics.Bool
Determine if a Push has been sent and is on it's way to the Elixir Channel.
pushInFlight
(\push -> push.event == "custom_event")
model.phoenix
pushWaiting : (PushConfig -> Basics.Bool) -> Model -> Basics.Bool
Determine 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.phoenix
timeoutPushes : Model -> Dict String (List PushConfig)
Pushes 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.
pushTimedOut : (PushConfig -> Basics.Bool) -> Model -> Basics.Bool
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.phoenix
dropTimeoutPush : (PushConfig -> Basics.Bool) -> Model -> Model
Cancel a timed out Push.
dropTimeoutPush
(\push -> push.topic == "topic:subTopic")
model.phoenix
This will only work after a push
has timed out and before it is re-tried.
pushTimeoutCountdown : (PushConfig -> Basics.Bool) -> Model -> Maybe Basics.Int
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.
dropPush : (PushConfig -> Basics.Bool) -> Model -> Model
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.
presenceState : Topic -> Model -> List Presence
A list of Presences on the Channel referenced by Topic.
presenceDiff : Topic -> Model -> List PresenceDiff
A list of Presence diffs on the Channel referenced by Topic.
presenceJoins : Topic -> Model -> List Presence
A list of Presences that have joined the Channel referenced by Topic.
presenceLeaves : Topic -> Model -> List Presence
A list of Presences that have left the Channel referenced by Topic.
lastPresenceJoin : Topic -> Model -> Maybe Presence
Maybe the last Presence to join the Channel referenced by Topic.
lastPresenceLeave : Topic -> Model -> Maybe Presence
Maybe the last Presence to leave the Channel referenced by Topic.
batch : List (Model -> ( Model, Platform.Cmd.Cmd Msg )) -> Model -> ( Model, Platform.Cmd.Cmd Msg )
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.phoenix
batchWithParams : List ( a -> Model -> ( Model, Platform.Cmd.Cmd Msg ), List a ) -> Model -> ( Model, Platform.Cmd.Cmd Msg )
Batch 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.phoenix
Here 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 : String -> String -> Json.Encode.Value -> Model -> Platform.Cmd.Cmd Msg
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 ]
...
}
startLogging : Model -> Platform.Cmd.Cmd Msg
Activate the socket's logger function. This will log all messages that the socket sends and receives.
stopLogging : Model -> Platform.Cmd.Cmd Msg
Deactivate the socket's logger function.