├── .gitignore ├── src ├── Phoenix │ ├── Serializer.elm │ ├── Internal │ │ ├── LongPollEvent.elm │ │ ├── Message.elm │ │ ├── Channel.elm │ │ ├── LongPoll.elm │ │ └── Socket.elm │ ├── Message.elm │ ├── Event.elm │ ├── Push.elm │ ├── Channel.elm │ └── Socket.elm └── Phoenix.elm ├── example ├── elm.json └── src │ └── Example.elm ├── elm.json ├── .travis.yml ├── README.md ├── LICENSE └── tests └── Phoenix ├── Internal └── SocketTest.elm ├── ChannelTest.elm └── SocketTest.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff 2 | -------------------------------------------------------------------------------- /src/Phoenix/Serializer.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Serializer exposing (Serializer(..)) 2 | 3 | 4 | type Serializer 5 | = V1 6 | | V2 7 | -------------------------------------------------------------------------------- /src/Phoenix/Internal/LongPollEvent.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.LongPollEvent exposing (LongPollEvent, longPolldecoder) 2 | 3 | import Json.Decode as Decode exposing (Decoder, field, int, list, map3, maybe, string) 4 | import Phoenix.Event as Event exposing (Event) 5 | 6 | 7 | type alias LongPollEvent = 8 | { status : Int 9 | , token : Maybe String 10 | , messages : Maybe (List Event) 11 | } 12 | 13 | 14 | longPolldecoder : Decoder LongPollEvent 15 | longPolldecoder = 16 | map3 LongPollEvent 17 | (field "status" int) 18 | (maybe (field "token" string)) 19 | (maybe (field "messages" (list Event.decoder))) 20 | -------------------------------------------------------------------------------- /example/elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "application", 3 | "source-directories": [ 4 | "src", 5 | "../src/" 6 | ], 7 | "elm-version": "0.19.0", 8 | "dependencies": { 9 | "direct": { 10 | "elm/browser": "1.0.0", 11 | "elm/core": "1.0.0", 12 | "elm/html": "1.0.0", 13 | "elm/http": "1.0.0", 14 | "elm/json": "1.0.0", 15 | "elm/regex": "1.0.0", 16 | "elm/time": "1.0.0" 17 | }, 18 | "indirect": { 19 | "elm/url": "1.0.0", 20 | "elm/virtual-dom": "1.0.0" 21 | } 22 | }, 23 | "test-dependencies": { 24 | "direct": {}, 25 | "indirect": {} 26 | } 27 | } -------------------------------------------------------------------------------- /elm.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "package", 3 | "name": "slashmili/phoenix-socket", 4 | "summary": "Elm client for Phoenix channels", 5 | "license": "MIT", 6 | "version": "4.2.1", 7 | "exposed-modules": [ 8 | "Phoenix", 9 | "Phoenix.Socket", 10 | "Phoenix.Channel", 11 | "Phoenix.Message", 12 | "Phoenix.Push" 13 | ], 14 | "elm-version": "0.19.0 <= v < 0.20.0", 15 | "dependencies": { 16 | "elm/core": "1.0.0 <= v < 2.0.0", 17 | "elm/http": "1.0.0 <= v < 2.0.0", 18 | "elm/json": "1.0.0 <= v < 2.0.0", 19 | "elm/regex": "1.0.0 <= v < 2.0.0", 20 | "elm/time": "1.0.0 <= v < 2.0.0" 21 | }, 22 | "test-dependencies": { 23 | "elm-explorations/test": "1.0.0 <= v < 2.0.0" 24 | } 25 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | cache: 4 | directories: 5 | - tests/elm-stuff/build-artifacts 6 | - sysconfcpus 7 | 8 | before_install: 9 | - if [ ${TRAVIS_OS_NAME} == "osx" ]; 10 | then brew update; brew install nvm; mkdir ~/.nvm; export NVM_DIR=~/.nvm; source $(brew --prefix nvm)/nvm.sh; 11 | fi 12 | - echo -e "Host github.com\n\tStrictHostKeyChecking no\n" >> ~/.ssh/config 13 | - | # epic build time improvement - see https://github.com/elm-lang/elm-compiler/issues/1473#issuecomment-245704142 14 | if [ ! -d sysconfcpus/bin ]; 15 | then 16 | git clone https://github.com/obmarg/libsysconfcpus.git; 17 | cd libsysconfcpus; 18 | ./configure --prefix=$TRAVIS_BUILD_DIR/sysconfcpus; 19 | make && make install; 20 | cd ..; 21 | fi 22 | install: 23 | - npm install -g elm@0.19.0 elm-test@0.19.0-beta4 elm-format@0.8.0 24 | 25 | script: 26 | - elm-format --validate src tests 27 | - elm make 28 | - $TRAVIS_BUILD_DIR/sysconfcpus/bin/sysconfcpus -n 2 elm-test 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Phoenix Socket 2 | This library is an [Elm](http://elm-lang.org/) client for [Phoenix](https://github.com/phoenixframework/phoenix) [channels](https://hexdocs.pm/phoenix/channels.html). 3 | 4 | This package is not ready to be used in production! However you can use it with Elm 0.19 right now, considering elm/Websocket is not out yet. 5 | 6 | 7 | The reasons you can use this package in Elm 0.19 8 | 9 | 1. It doesn't use Effects Manager. 10 | 2. Initially it supports both Websocket and LongPool transport layer. With the lack of Websocket package, the code related to Websocket is removed and you can use this package today in your Elm 0.19 project. 11 | 12 | 13 | Take a look at `example` directory for a detailed example. 14 | 15 | ## Credit 16 | Most of the codes an ideas are borrowed from [fbonetti/elm-phoenix-socket](https://github.com/fbonetti/elm-phoenix-socket) 17 | 18 | 19 | ## Goals 20 | * I have started this project merely for the sake learning Elm. 21 | * Make it easier to work with [jamesmacaulay/elm-graphql](https://github.com/jamesmacaulay/elm-graphql) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Frank Bonetti 4 | Copyright (c) 2018 Milad Rastian 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /src/Phoenix/Internal/Message.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.Message exposing (InternalMessage(..), channelClosed, channelError, channelFailedToJoin, channelSuccessfullyJoined, none) 2 | 3 | import Http 4 | import Json.Decode as Decode 5 | import Phoenix.Channel exposing (Channel) 6 | import Phoenix.Event exposing (Event) 7 | import Phoenix.Internal.LongPollEvent exposing (LongPollEvent) 8 | import Time 9 | 10 | 11 | type InternalMessage msg 12 | = NoOp 13 | | ChannelSuccessfullyJoined (Channel msg) Decode.Value 14 | | ChannelFailedToJoin (Channel msg) Decode.Value 15 | | ChannelClosed (Channel msg) Decode.Value 16 | | ChannelError (Channel msg) Decode.Value 17 | | HeartbeatReply 18 | | LongPollTick Time.Posix 19 | | LongPollSent (Result Http.Error LongPollEvent) 20 | | LongPollPolled (Result Http.Error LongPollEvent) 21 | | Heartbeat Time.Posix 22 | 23 | 24 | channelSuccessfullyJoined : Channel msg -> Decode.Value -> InternalMessage msg 25 | channelSuccessfullyJoined channel response = 26 | ChannelSuccessfullyJoined channel response 27 | 28 | 29 | channelFailedToJoin : Channel msg -> Decode.Value -> InternalMessage msg 30 | channelFailedToJoin channel response = 31 | ChannelFailedToJoin channel response 32 | 33 | 34 | none : InternalMessage msg 35 | none = 36 | NoOp 37 | 38 | 39 | channelClosed : Decode.Value -> Channel msg -> InternalMessage msg 40 | channelClosed response channel = 41 | ChannelClosed channel response 42 | 43 | 44 | channelError : Decode.Value -> Channel msg -> InternalMessage msg 45 | channelError response channel = 46 | ChannelError channel response 47 | -------------------------------------------------------------------------------- /src/Phoenix/Internal/Channel.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.Channel exposing (onClosedCommand, onCustomCommand, onErrorCommand, onFailedToJoinCommand, onJoinedCommand) 2 | 3 | import Dict 4 | import Json.Decode as Decode 5 | import Phoenix.Channel as Channel exposing (Channel) 6 | import Phoenix.Message as Message exposing (Msg) 7 | import Phoenix.Push exposing (Push) 8 | import Task 9 | 10 | 11 | onReceiveMsg : String -> Decode.Value -> Channel msg -> Msg msg 12 | onReceiveMsg name response channel = 13 | case Dict.get name (Channel.receives channel) of 14 | Just cmd -> 15 | Message.toExternalMsg (cmd response) 16 | 17 | _ -> 18 | Message.none 19 | 20 | 21 | onJoinedCommand : Decode.Value -> Channel msg -> Msg msg 22 | onJoinedCommand response channel = 23 | onReceiveMsg "ok" response channel 24 | 25 | 26 | onFailedToJoinCommand : Decode.Value -> Channel msg -> Msg msg 27 | onFailedToJoinCommand response channel = 28 | onReceiveMsg "join_error" response channel 29 | 30 | 31 | onClosedCommand : Decode.Value -> Channel msg -> Msg msg 32 | onClosedCommand response channel = 33 | onReceiveMsg "close" response channel 34 | 35 | 36 | onErrorCommand : Decode.Value -> Channel msg -> Msg msg 37 | onErrorCommand response channel = 38 | onReceiveMsg "error" response channel 39 | 40 | 41 | onCustomCommand : String -> Decode.Value -> Channel msg -> Msg msg 42 | onCustomCommand eventName response channel = 43 | case Dict.get eventName (Channel.ons channel) of 44 | Just cmd -> 45 | Message.toExternalMsg (cmd response) 46 | 47 | _ -> 48 | Message.none 49 | -------------------------------------------------------------------------------- /src/Phoenix/Message.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Message exposing (Msg, mapAll, none, toInternalMsg, toExternalMsg, extractInternalMsg, extractExternalMsg) 2 | 3 | {-| 4 | 5 | 6 | # This module provides Msg that the package handles 7 | 8 | @docs Msg, mapAll, none, toInternalMsg, toExternalMsg, extractInternalMsg, extractExternalMsg 9 | 10 | -} 11 | 12 | import Http 13 | import Json.Decode as Decode 14 | import Phoenix.Channel exposing (Channel) 15 | import Phoenix.Event exposing (Event) 16 | import Phoenix.Internal.Message as InternalMessage exposing (InternalMessage(..)) 17 | 18 | 19 | {-| This Msg should be used in user's main app 20 | 21 | import Phoenix.Message as PhxMsg 22 | 23 | type MyAppMsg = 24 | .. 25 | | PhoenixMsg (PhxMsg.Msg MyAppMsg) 26 | 27 | -} 28 | type Msg msg 29 | = NoOp 30 | | ExternalMsg msg 31 | | InternalMsg (InternalMessage msg) 32 | 33 | 34 | {-| -} 35 | mapAll : (Msg msg -> msg) -> Msg msg -> msg 36 | mapAll fn internalMsg = 37 | case internalMsg of 38 | ExternalMsg msg -> 39 | msg 40 | 41 | _ -> 42 | fn internalMsg 43 | 44 | 45 | {-| -} 46 | none : Msg msg 47 | none = 48 | NoOp 49 | 50 | 51 | {-| -} 52 | toExternalMsg : msg -> Msg msg 53 | toExternalMsg externalMsg = 54 | ExternalMsg externalMsg 55 | 56 | 57 | {-| -} 58 | toInternalMsg : InternalMessage msg -> Msg msg 59 | toInternalMsg internalMsg = 60 | InternalMsg internalMsg 61 | 62 | 63 | {-| -} 64 | extractInternalMsg : Msg msg -> InternalMessage msg 65 | extractInternalMsg publicMsg = 66 | case publicMsg of 67 | InternalMsg msg -> 68 | msg 69 | 70 | ExternalMsg msg -> 71 | InternalMessage.none 72 | 73 | NoOp -> 74 | InternalMessage.none 75 | 76 | 77 | {-| -} 78 | extractExternalMsg : Msg msg -> Maybe msg 79 | extractExternalMsg publicMsg = 80 | case publicMsg of 81 | ExternalMsg msg -> 82 | Just msg 83 | 84 | _ -> 85 | Nothing 86 | -------------------------------------------------------------------------------- /src/Phoenix.elm: -------------------------------------------------------------------------------- 1 | module Phoenix exposing (listen, update, join, push, subscribe) 2 | 3 | {-| 4 | 5 | 6 | # Basic Usage 7 | 8 | @docs listen, update, join, push, subscribe 9 | 10 | -} 11 | 12 | import Json.Encode as Encode 13 | import Phoenix.Channel as Channel exposing (Channel) 14 | import Phoenix.Message as Message exposing (Msg) 15 | import Phoenix.Push as Push exposing (Push) 16 | import Phoenix.Socket as Socket exposing (Socket) 17 | 18 | 19 | {-| Listens to socket change and timers 20 | -} 21 | listen : (Msg msg -> msg) -> Socket msg -> Sub msg 22 | listen toExternalAppMsgFn socket = 23 | Socket.listen toExternalAppMsgFn socket 24 | 25 | 26 | {-| Updates socket model and send messages 27 | -} 28 | update : (Msg msg -> msg) -> Msg msg -> Socket msg -> ( Socket msg, Cmd msg ) 29 | update toExternalAppMsgFn msg socket = 30 | Socket.update toExternalAppMsgFn msg socket 31 | 32 | 33 | {-| Joins a channel 34 | -} 35 | join : (Msg msg -> msg) -> Channel msg -> Socket msg -> ( Socket msg, Cmd msg ) 36 | join toExternalAppMsgFn channel socket = 37 | let 38 | ( updateSocket, phxCmd ) = 39 | Socket.join channel socket 40 | in 41 | ( updateSocket, Cmd.map toExternalAppMsgFn phxCmd ) 42 | 43 | 44 | {-| Subscribes to a channel 45 | 46 | It's different than join and only Subscribes to a channel events without triggering join on remote server 47 | 48 | -} 49 | subscribe : (Msg msg -> msg) -> Channel msg -> Socket msg -> ( Socket msg, Cmd msg ) 50 | subscribe toExternalAppMsgFn channel socket = 51 | let 52 | ( updateSocket, phxCmd ) = 53 | Socket.subscribe channel socket 54 | in 55 | ( updateSocket, Cmd.none ) 56 | 57 | 58 | {-| Pushes a a message 59 | -} 60 | push : (Msg msg -> msg) -> Push msg -> Socket msg -> ( Socket msg, Cmd msg ) 61 | push toExternalAppMsgFn pushRecord socket = 62 | let 63 | ( updateSocket, phxCmd ) = 64 | Socket.push pushRecord socket 65 | in 66 | ( updateSocket, Cmd.map toExternalAppMsgFn phxCmd ) 67 | -------------------------------------------------------------------------------- /src/Phoenix/Event.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Event exposing (Event, decode, decodeReply, decoder, encode, encoder, init) 2 | 3 | import Json.Decode as Decode exposing (Decoder, field, maybe) 4 | import Json.Encode as Encode 5 | 6 | 7 | type alias Event = 8 | { event : String 9 | , topic : String 10 | , payload : Decode.Value 11 | , ref : Maybe Int 12 | } 13 | 14 | 15 | init : String -> String -> Decode.Value -> Maybe Int -> Event 16 | init event topic payload ref = 17 | { event = event, topic = topic, payload = payload, ref = ref } 18 | 19 | 20 | decode : String -> Maybe Event 21 | decode = 22 | Decode.decodeString decoder >> Result.toMaybe 23 | 24 | 25 | decoder : Decoder Event 26 | decoder = 27 | Decode.map4 Event 28 | (field "event" Decode.string) 29 | (field "topic" Decode.string) 30 | (field "payload" Decode.value) 31 | (field "ref" (maybe Decode.int)) 32 | 33 | 34 | encode : Event -> String 35 | encode = 36 | encoder >> Encode.encode 0 37 | 38 | 39 | encoder : Event -> Encode.Value 40 | encoder { event, topic, payload, ref } = 41 | Encode.object 42 | [ ( "event", Encode.string event ) 43 | , ( "topic", Encode.string topic ) 44 | , ( "payload", payload ) 45 | , ( "ref", maybeInt ref ) 46 | ] 47 | 48 | 49 | maybeInt int_ = 50 | int_ 51 | |> Maybe.map Encode.int 52 | |> Maybe.withDefault Encode.null 53 | 54 | 55 | replyDecoder : Decode.Decoder (Result Decode.Value Decode.Value) 56 | replyDecoder = 57 | Decode.map2 statusToResult 58 | (field "status" Decode.string) 59 | (field "response" Decode.value) 60 | 61 | 62 | decodeReply : Decode.Value -> Result Decode.Value Decode.Value 63 | decodeReply payload = 64 | case Decode.decodeValue replyDecoder payload of 65 | Ok (Ok response) -> 66 | Ok response 67 | 68 | Ok (Err response) -> 69 | Err response 70 | 71 | Err errMsg -> 72 | Err (Encode.object [ ( "reason", Encode.string "failed to parse response" ) ]) 73 | 74 | 75 | statusToResult : String -> Decode.Value -> Result Decode.Value Decode.Value 76 | statusToResult status response = 77 | if status == "ok" then 78 | Ok response 79 | 80 | else 81 | Err response 82 | -------------------------------------------------------------------------------- /src/Phoenix/Push.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Push exposing (Push, init, initWithChannel, onError, onErrorCommand, onOk, onOkCommand, withPayload) 2 | 3 | {-| 4 | 5 | 6 | # This module provides an interface for pushed messages 7 | 8 | @docs Push, init, initWithChannel, onError, onErrorCommand, onOk, onOkCommand, withPayload 9 | 10 | -} 11 | 12 | import Dict exposing (Dict) 13 | import Json.Decode as Decode 14 | import Json.Encode as Encode 15 | import Phoenix.Channel as Channel exposing (Channel) 16 | import Phoenix.Message as Message exposing (Msg) 17 | 18 | 19 | {-| Push model 20 | -} 21 | type alias Push msg = 22 | { event : String 23 | , topic : String 24 | , on : Dict String (Decode.Value -> msg) 25 | , payload : Encode.Value 26 | } 27 | 28 | 29 | {-| initializes Push with event name and the channel 30 | -} 31 | init : String -> String -> Push msg 32 | init event topic = 33 | { event = event, topic = topic, payload = Encode.object [], on = Dict.empty } 34 | 35 | 36 | {-| initializes Push with event name and the channel 37 | -} 38 | initWithChannel : String -> Channel msg -> Push msg 39 | initWithChannel event channel = 40 | { event = event, topic = Channel.topic channel, payload = Encode.object [], on = Dict.empty } 41 | 42 | 43 | {-| Adds payload to Push 44 | -} 45 | withPayload : Encode.Value -> Push msg -> Push msg 46 | withPayload payload push = 47 | { push | payload = payload } 48 | 49 | 50 | {-| When Channel replies with ok this message gets triggred 51 | -} 52 | onOk : (Decode.Value -> msg) -> Push msg -> Push msg 53 | onOk valueToMsg push = 54 | let 55 | on = 56 | Dict.insert "ok" valueToMsg push.on 57 | in 58 | { push | on = on } 59 | 60 | 61 | {-| When Channel replies with errro this message gets triggred 62 | -} 63 | onError : (Decode.Value -> msg) -> Push msg -> Push msg 64 | onError valueToMsg push = 65 | let 66 | on = 67 | Dict.insert "error" valueToMsg push.on 68 | in 69 | { push | on = on } 70 | 71 | 72 | onReceiveMsg : String -> Decode.Value -> Push msg -> Msg msg 73 | onReceiveMsg name response push = 74 | case Dict.get name push.on of 75 | Just cmd -> 76 | Message.toExternalMsg (cmd response) 77 | 78 | _ -> 79 | Message.none 80 | 81 | 82 | {-| Convert an Ok response to Message 83 | -} 84 | onOkCommand : Decode.Value -> Push msg -> Msg msg 85 | onOkCommand response push = 86 | onReceiveMsg "ok" response push 87 | 88 | 89 | {-| Convert a Error response to Message 90 | -} 91 | onErrorCommand : Decode.Value -> Push msg -> Msg msg 92 | onErrorCommand response push = 93 | onReceiveMsg "error" response push 94 | -------------------------------------------------------------------------------- /src/Phoenix/Internal/LongPoll.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.LongPoll exposing (LongPollResponse, externalMsgs, poll, send) 2 | 3 | import Dict exposing (Dict) 4 | import Http 5 | import Json.Encode as Encode 6 | import Phoenix.Channel as Channel exposing (Channel) 7 | import Phoenix.Event as Event exposing (Event) 8 | import Phoenix.Internal.Channel as ChannelHelper 9 | import Phoenix.Internal.LongPollEvent exposing (longPolldecoder) 10 | import Phoenix.Internal.Message as InternalMessage exposing (InternalMessage(..)) 11 | import Phoenix.Internal.Socket as SocketHelper 12 | import Phoenix.Message as Message exposing (Msg) 13 | import Phoenix.Push exposing (Push) 14 | import Task 15 | 16 | 17 | type alias LongPollResponse = 18 | { status : Int 19 | , token : String 20 | } 21 | 22 | 23 | send : String -> Maybe String -> Event -> Cmd (Msg msg) 24 | send endPoint maybeToken event = 25 | let 26 | qparam = 27 | case maybeToken of 28 | Just token -> 29 | "?token=" ++ token 30 | 31 | _ -> 32 | "" 33 | in 34 | longPolldecoder 35 | |> Http.post (endPoint ++ qparam) (Http.jsonBody (Event.encoder event)) 36 | |> Http.send LongPollSent 37 | |> Cmd.map Message.toInternalMsg 38 | 39 | 40 | poll : String -> List ( String, String ) -> Maybe String -> Cmd (Msg msg) 41 | poll endPoint payload maybeToken = 42 | let 43 | qparams = 44 | case maybeToken of 45 | Nothing -> 46 | payload 47 | |> List.map (\( key, value ) -> key ++ "=" ++ value) 48 | |> String.join "&" 49 | 50 | Just connectionToken -> 51 | "token=" ++ connectionToken 52 | 53 | fullQueryParams = 54 | if String.length qparams == 0 then 55 | "" 56 | 57 | else 58 | "?" ++ qparams 59 | in 60 | Http.get (endPoint ++ fullQueryParams) longPolldecoder 61 | |> Http.send LongPollPolled 62 | |> Cmd.map Message.toInternalMsg 63 | 64 | 65 | externalMsgs : Dict Int (Push msg) -> Dict.Dict String (Channel msg) -> (Msg msg -> msg) -> List Event -> Cmd msg 66 | externalMsgs pushedEvents channels toExternalAppMsgFn events = 67 | let 68 | mappedMsg = 69 | Message.mapAll toExternalAppMsgFn 70 | 71 | msgs = 72 | events 73 | |> List.reverse 74 | |> List.map (\e -> SocketHelper.mapExternalEvents pushedEvents channels e) 75 | in 76 | msgs 77 | |> List.map (\m -> m |> Task.succeed |> Task.perform identity) 78 | |> Cmd.batch 79 | |> Cmd.map mappedMsg 80 | -------------------------------------------------------------------------------- /src/Phoenix/Internal/Socket.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.Socket exposing (handleInternalPhxReply, heartbeatSubscription, mapExternalEvents, mapInternalEvents, mapMaybeExternalEvents, mapMaybeInternalEvents) 2 | 3 | import Dict exposing (Dict) 4 | import Json.Encode as Encode 5 | import Phoenix.Channel as Channel exposing (Channel) 6 | import Phoenix.Event as Event exposing (Event) 7 | import Phoenix.Internal.Channel as ChannelHelper 8 | import Phoenix.Internal.Message as InternalMessage exposing (InternalMessage(..)) 9 | import Phoenix.Message as Message exposing (Msg) 10 | import Phoenix.Push as Push exposing (Push) 11 | import Time 12 | 13 | 14 | mapMaybeInternalEvents : Dict.Dict String (Channel msg) -> Maybe Event -> Msg msg 15 | mapMaybeInternalEvents channels maybeEvent = 16 | case maybeEvent of 17 | Just event -> 18 | case ( event.topic, event.event ) of 19 | ( "phoenix", "phx_reply" ) -> 20 | Message.toInternalMsg HeartbeatReply 21 | 22 | _ -> 23 | mapInternalEvents channels event 24 | 25 | Nothing -> 26 | Message.none 27 | 28 | 29 | mapInternalEvents : Dict.Dict String (Channel msg) -> Event -> Msg msg 30 | mapInternalEvents channels event = 31 | let 32 | channel = 33 | Channel.findChannel event.topic 34 | in 35 | case event.event of 36 | "phx_reply" -> 37 | handleInternalPhxReply channels event 38 | 39 | "phx_close" -> 40 | channels 41 | |> channel 42 | |> Maybe.andThen (\chan -> Just (Message.toInternalMsg (InternalMessage.channelClosed event.payload chan))) 43 | |> Maybe.withDefault Message.none 44 | 45 | "phx_error" -> 46 | channels 47 | |> channel 48 | |> Maybe.andThen (\chan -> Just (Message.toInternalMsg (InternalMessage.channelError event.payload chan))) 49 | |> Maybe.withDefault Message.none 50 | 51 | _ -> 52 | Message.none 53 | 54 | 55 | mapMaybeExternalEvents : Dict Int (Push msg) -> Dict.Dict String (Channel msg) -> Maybe Event -> Msg msg 56 | mapMaybeExternalEvents pushedEvents channels maybeEvent = 57 | case maybeEvent of 58 | Just event -> 59 | mapExternalEvents pushedEvents channels event 60 | 61 | Nothing -> 62 | Message.none 63 | 64 | 65 | mapExternalEvents : Dict Int (Push msg) -> Dict.Dict String (Channel msg) -> Event -> Msg msg 66 | mapExternalEvents pushedEvents channels event = 67 | let 68 | channelWithRef = 69 | Channel.findChannelWithRef event.topic event.ref 70 | 71 | channel = 72 | Channel.findChannel event.topic 73 | in 74 | case event.event of 75 | "phx_reply" -> 76 | case channelWithRef channels of 77 | Just chan -> 78 | case Event.decodeReply event.payload of 79 | Ok response -> 80 | ChannelHelper.onJoinedCommand response chan 81 | 82 | Err response -> 83 | ChannelHelper.onFailedToJoinCommand response chan 84 | 85 | Nothing -> 86 | case event.ref of 87 | Just ref -> 88 | case Dict.get ref pushedEvents of 89 | Just push -> 90 | case Event.decodeReply event.payload of 91 | Ok response -> 92 | Push.onOkCommand response push 93 | 94 | Err response -> 95 | Push.onErrorCommand response push 96 | 97 | _ -> 98 | Message.none 99 | 100 | _ -> 101 | Message.none 102 | 103 | "phx_error" -> 104 | channels 105 | |> channel 106 | |> Maybe.andThen (\chan -> Just (ChannelHelper.onErrorCommand event.payload chan)) 107 | |> Maybe.withDefault Message.none 108 | 109 | "phx_close" -> 110 | channels 111 | |> channel 112 | |> Maybe.andThen (\chan -> Just (ChannelHelper.onClosedCommand event.payload chan)) 113 | |> Maybe.withDefault Message.none 114 | 115 | _ -> 116 | channels 117 | |> channel 118 | |> Maybe.andThen (\chan -> Just (ChannelHelper.onCustomCommand event.event event.payload chan)) 119 | |> Maybe.withDefault Message.none 120 | 121 | 122 | handleInternalPhxReply : Dict.Dict String (Channel msg) -> Event -> Msg msg 123 | handleInternalPhxReply channels event = 124 | case Channel.findChannelWithRef event.topic event.ref channels of 125 | Just channel -> 126 | case Event.decodeReply event.payload of 127 | Ok response -> 128 | Message.toInternalMsg (InternalMessage.channelSuccessfullyJoined channel response) 129 | 130 | Err response -> 131 | Message.toInternalMsg (InternalMessage.channelFailedToJoin channel response) 132 | 133 | Nothing -> 134 | Message.none 135 | 136 | 137 | heartbeatSubscription : Float -> Sub (Msg msg) 138 | heartbeatSubscription heartbeatIntervalSeconds = 139 | Heartbeat 140 | |> Time.every (1000 * heartbeatIntervalSeconds) 141 | |> Sub.map Message.toInternalMsg 142 | -------------------------------------------------------------------------------- /example/src/Example.elm: -------------------------------------------------------------------------------- 1 | module Example exposing (main) 2 | 3 | import Browser 4 | import Html exposing (..) 5 | import Html.Attributes exposing (..) 6 | import Html.Events exposing (..) 7 | import Json.Decode as Decode exposing (Value) 8 | import Json.Encode as Encode 9 | import Phoenix 10 | import Phoenix.Channel as Channel 11 | import Phoenix.Message as PhxMsg 12 | import Phoenix.Push as Push 13 | import Phoenix.Socket as Socket 14 | 15 | 16 | main = 17 | Browser.element 18 | { init = init 19 | , view = view 20 | , update = update 21 | , subscriptions = subscriptions 22 | } 23 | 24 | 25 | 26 | -- MODEL 27 | 28 | 29 | type alias Model = 30 | { input : String 31 | , messages : List String 32 | , phxSocket : Socket.Socket Msg 33 | } 34 | 35 | 36 | endPoint = 37 | "ws://localhost:4001/socket/websocket" 38 | 39 | 40 | initSocket = 41 | endPoint 42 | |> Socket.init 43 | |> Socket.withLongPoll 44 | |> Socket.withPayload [ ( "access_token", "super_secret_t0ken!!!" ) ] 45 | 46 | 47 | init : () -> ( Model, Cmd Msg ) 48 | init _ = 49 | ( Model "" [] initSocket 50 | , Cmd.none 51 | ) 52 | 53 | 54 | 55 | -- UPDATE 56 | 57 | 58 | type Msg 59 | = Input String 60 | | JoinChannel 61 | | NewMessage String 62 | | NewJsonMessage Value 63 | | JoinedChannel Value 64 | | FailedToJoinedChannel Value 65 | | FailedToConnectChannel Value 66 | | ClosedChannel Value 67 | | SendMessage 68 | | LeaveChannel 69 | | GetType 70 | | PhoenixMsg (PhxMsg.Msg Msg) 71 | 72 | 73 | channel = 74 | Channel.init "numbers:positive" 75 | |> Channel.withPayload (Encode.object [ ( "initial_message", Encode.string "hello channel" ) ]) 76 | |> Channel.onJoin JoinedChannel 77 | |> Channel.onJoinError FailedToJoinedChannel 78 | |> Channel.onError FailedToConnectChannel 79 | |> Channel.onClose ClosedChannel 80 | |> Channel.on "update" NewJsonMessage 81 | 82 | 83 | update : Msg -> Model -> ( Model, Cmd Msg ) 84 | update msg model = 85 | --update msg { input, messages, phxSocket } = 86 | case msg of 87 | Input newInput -> 88 | ( { model | input = newInput }, Cmd.none ) 89 | 90 | JoinChannel -> 91 | let 92 | ( updatedSocketModel, newCommand ) = 93 | Phoenix.join PhoenixMsg channel model.phxSocket 94 | in 95 | ( { model | input = "", phxSocket = updatedSocketModel }, newCommand ) 96 | 97 | GetType -> 98 | let 99 | push = 100 | channel 101 | |> Push.initWithChannel "type" 102 | |> Push.withPayload payload 103 | 104 | payload = 105 | Encode.object [] 106 | 107 | ( updatedSocketModel, newCommand ) = 108 | Phoenix.push PhoenixMsg push model.phxSocket 109 | in 110 | ( { model | phxSocket = updatedSocketModel }, newCommand ) 111 | 112 | LeaveChannel -> 113 | let 114 | payload = 115 | Encode.object [] 116 | 117 | push = 118 | channel 119 | |> Push.initWithChannel "phx_leave" 120 | |> Push.withPayload payload 121 | 122 | ( updatedSocketModel, newCommand ) = 123 | Phoenix.push PhoenixMsg push model.phxSocket 124 | in 125 | ( { model | phxSocket = updatedSocketModel }, newCommand ) 126 | 127 | SendMessage -> 128 | let 129 | payload = 130 | Encode.object [ ( "name", Encode.string "boo" ) ] 131 | 132 | push = 133 | "numbers:positive" 134 | |> Push.init "new:msg" 135 | |> Push.withPayload payload 136 | 137 | ( updatedSocketModel, newCommand ) = 138 | Phoenix.push PhoenixMsg push model.phxSocket 139 | in 140 | ( { model | phxSocket = updatedSocketModel }, newCommand ) 141 | 142 | NewMessage str -> 143 | ( { model | messages = str :: model.messages }, Cmd.none ) 144 | 145 | NewJsonMessage json -> 146 | ( { model | messages = toString json :: model.messages }, Cmd.none ) 147 | 148 | JoinedChannel json -> 149 | ( { model | messages = "Joined channel with data: " :: toString json :: model.messages }, Cmd.none ) 150 | 151 | ClosedChannel json -> 152 | ( { model | messages = "ClosedChannel : " :: toString json :: model.messages }, Cmd.none ) 153 | 154 | FailedToJoinedChannel json -> 155 | ( { model | messages = "Failed to joined channel with data: " :: toString json :: model.messages }, Cmd.none ) 156 | 157 | FailedToConnectChannel json -> 158 | ( { model | messages = "Failed to connect: " :: toString json :: model.messages }, Cmd.none ) 159 | 160 | PhoenixMsg innerMsg -> 161 | let 162 | ( updatedSocketModel, newCommand ) = 163 | Phoenix.update PhoenixMsg innerMsg model.phxSocket 164 | in 165 | ( { model | phxSocket = updatedSocketModel }, newCommand ) 166 | 167 | 168 | toString json = 169 | Encode.encode 0 json 170 | 171 | 172 | 173 | -- SUBSCRIPTIONS 174 | 175 | 176 | subscriptions : Model -> Sub Msg 177 | subscriptions model = 178 | Sub.batch 179 | [ Phoenix.listen PhoenixMsg model.phxSocket 180 | ] 181 | 182 | 183 | 184 | -- VIEW 185 | 186 | 187 | view : Model -> Html Msg 188 | view model = 189 | div [] 190 | [ div [] (List.map viewMessage model.messages) 191 | , input [ onInput Input ] [] 192 | , button [ onClick JoinChannel ] [ text "Join" ] 193 | , button [ onClick SendMessage ] [ text "send" ] 194 | , button [ onClick LeaveChannel ] [ text "Leave" ] 195 | , button [ onClick GetType ] [ text "get current type" ] 196 | ] 197 | 198 | 199 | viewMessage : String -> Html msg 200 | viewMessage msg = 201 | div [] [ text msg ] 202 | -------------------------------------------------------------------------------- /src/Phoenix/Channel.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Channel exposing (Channel, init, setJoiningState, setClosedState, setJoinedState, setErroredState, isOngoing, isClosed, isJoined, isErrored, addChannel, updateChannel, updateChannelDict, findChannelWithRef, findChannel, on, onJoin, onJoinError, onError, onClose, topic, joinRef, receives, ons, payload, withPayload) 2 | 3 | {-| 4 | 5 | 6 | # This module is keeping states related to channel 7 | 8 | @docs Channel, init, setJoiningState, setClosedState, setJoinedState, setErroredState, isOngoing, isClosed, isJoined, isErrored, addChannel, updateChannel, updateChannelDict, findChannelWithRef, findChannel, on, onJoin, onJoinError, onError, onClose, topic, joinRef, receives, ons, payload, withPayload 9 | 10 | -} 11 | 12 | import Dict exposing (Dict) 13 | import Json.Decode as Decode exposing (Value) 14 | import Json.Encode as Encode 15 | 16 | 17 | type State 18 | = Init 19 | | Closed 20 | | Errored 21 | | Joined 22 | | Joining 23 | | Leaving 24 | 25 | 26 | {-| Channel Model 27 | -} 28 | type Channel msg 29 | = Channel 30 | { topic : String 31 | , on : Dict String (Value -> msg) 32 | , state : State 33 | , joinRef : Maybe Int 34 | , receive : Dict String (Value -> msg) 35 | , payload : Encode.Value 36 | } 37 | 38 | 39 | {-| Init channel model using channel topic 40 | -} 41 | init : String -> Channel msg 42 | init channelTopic = 43 | Channel 44 | { topic = channelTopic 45 | , on = Dict.empty 46 | , state = Init 47 | , joinRef = Nothing 48 | , receive = Dict.empty 49 | , payload = Encode.object [] 50 | } 51 | 52 | 53 | {-| Includes payload during initializing the channel 54 | -} 55 | withPayload : Value -> Channel msg -> Channel msg 56 | withPayload payloadVal (Channel channel) = 57 | Channel { channel | payload = payloadVal } 58 | 59 | 60 | {-| Sets the joining reference and state to Joining 61 | -} 62 | setJoiningState : Int -> Channel msg -> Channel msg 63 | setJoiningState ref (Channel channel) = 64 | Channel { channel | state = Joining, joinRef = Just ref } 65 | 66 | 67 | {-| Sets stats to joined state 68 | -} 69 | setJoinedState : Channel msg -> Channel msg 70 | setJoinedState (Channel channel) = 71 | Channel { channel | state = Joined, joinRef = Nothing } 72 | 73 | 74 | {-| Sets stats to Errored 75 | -} 76 | setErroredState : Channel msg -> Channel msg 77 | setErroredState (Channel channel) = 78 | Channel { channel | state = Errored, joinRef = Nothing } 79 | 80 | 81 | {-| Sets stats to Closed 82 | -} 83 | setClosedState : Channel msg -> Channel msg 84 | setClosedState (Channel channel) = 85 | Channel { channel | state = Closed, joinRef = Nothing } 86 | 87 | 88 | {-| Returns true if state is Joined Joining 89 | -} 90 | isOngoing : Channel msg -> Bool 91 | isOngoing (Channel channel) = 92 | if channel.state == Joining || channel.state == Joined then 93 | True 94 | 95 | else 96 | False 97 | 98 | 99 | {-| Is this channel joined successfully ? 100 | -} 101 | isJoined : Channel msg -> Bool 102 | isJoined (Channel channel) = 103 | case channel.state of 104 | Joined -> 105 | True 106 | 107 | _ -> 108 | False 109 | 110 | 111 | {-| Is this channel closed ? 112 | -} 113 | isClosed : Channel msg -> Bool 114 | isClosed (Channel channel) = 115 | case channel.state of 116 | Closed -> 117 | True 118 | 119 | _ -> 120 | False 121 | 122 | 123 | {-| Is this channel faild to join 124 | -} 125 | isErrored : Channel msg -> Bool 126 | isErrored (Channel channel) = 127 | case channel.state of 128 | Errored -> 129 | True 130 | 131 | _ -> 132 | False 133 | 134 | 135 | {-| Adds a channel to Dict of channels 136 | -} 137 | addChannel : Channel msg -> Dict String (Channel msg) -> Dict String (Channel msg) 138 | addChannel (Channel channel) channelDict = 139 | Dict.insert channel.topic (Channel channel) channelDict 140 | 141 | 142 | {-| Finds a channel with its topic and joinRef number 143 | -} 144 | findChannelWithRef : String -> Maybe Int -> Dict String (Channel msg) -> Maybe (Channel msg) 145 | findChannelWithRef channelTopic channelJoinRef channels = 146 | case Dict.get channelTopic channels of 147 | Just (Channel channel) -> 148 | if channel.joinRef == channelJoinRef then 149 | Just (Channel channel) 150 | 151 | else 152 | Nothing 153 | 154 | _ -> 155 | Nothing 156 | 157 | 158 | {-| Finds a channel in Dict of channels by its topic 159 | -} 160 | findChannel : String -> Dict String (Channel msg) -> Maybe (Channel msg) 161 | findChannel channelTopic channels = 162 | Dict.get channelTopic channels 163 | 164 | 165 | {-| Updates channel in the given Dict 166 | -} 167 | updateChannel : Channel msg -> Dict String (Channel msg) -> Dict String (Channel msg) 168 | updateChannel (Channel channel) channels = 169 | Dict.insert channel.topic (Channel channel) channels 170 | 171 | 172 | {-| Triggers this message to send when joined a channel 173 | -} 174 | onJoin : (Value -> msg) -> Channel msg -> Channel msg 175 | onJoin valueToMsg channel = 176 | receive "ok" valueToMsg channel 177 | 178 | 179 | {-| Triggers this message to send when failed to join a channel 180 | -} 181 | onJoinError : (Value -> msg) -> Channel msg -> Channel msg 182 | onJoinError valueToMsg channel = 183 | receive "join_error" valueToMsg channel 184 | 185 | 186 | {-| Triggers this message when failed to send join command on the connection 187 | 188 | onError messages are invoked if the channel crashes on the server. 189 | 190 | -} 191 | onError : (Value -> msg) -> Channel msg -> Channel msg 192 | onError valueToMsg channel = 193 | receive "error" valueToMsg channel 194 | 195 | 196 | {-| Triggers this message when channel is closed 197 | 198 | onClose messages are invoked if the channel explicitly closed on the server. 199 | 200 | -} 201 | onClose : (Value -> msg) -> Channel msg -> Channel msg 202 | onClose valueToMsg channel = 203 | receive "close" valueToMsg channel 204 | 205 | 206 | {-| Triggers this message when event is received 207 | -} 208 | on : String -> (Value -> msg) -> Channel msg -> Channel msg 209 | on event cb (Channel channel) = 210 | Channel { channel | on = Dict.insert event cb channel.on } 211 | 212 | 213 | receive : String -> (Value -> msg) -> Channel msg -> Channel msg 214 | receive event valueToMsg (Channel channel) = 215 | Channel { channel | receive = Dict.insert event valueToMsg channel.receive } 216 | 217 | 218 | {-| -} 219 | updateChannelDict : Channel msg -> Dict String (Channel msg) -> Dict String (Channel msg) 220 | updateChannelDict channel channels = 221 | updateChannel channel channels 222 | 223 | 224 | {-| -} 225 | topic : Channel msg -> String 226 | topic (Channel channel) = 227 | channel.topic 228 | 229 | 230 | {-| -} 231 | joinRef : Channel msg -> Maybe Int 232 | joinRef (Channel channel) = 233 | channel.joinRef 234 | 235 | 236 | {-| -} 237 | receives : Channel msg -> Dict String (Value -> msg) 238 | receives (Channel channel) = 239 | channel.receive 240 | 241 | 242 | {-| -} 243 | ons : Channel msg -> Dict String (Value -> msg) 244 | ons (Channel channel) = 245 | channel.on 246 | 247 | 248 | {-| -} 249 | payload : Channel msg -> Encode.Value 250 | payload (Channel channel) = 251 | channel.payload 252 | -------------------------------------------------------------------------------- /tests/Phoenix/Internal/SocketTest.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Internal.SocketTest exposing (TestMsg(..), suite) 2 | 3 | import Dict 4 | import Expect exposing (Expectation) 5 | import Fuzz exposing (Fuzzer, int, list, string) 6 | import Json.Encode as Encode 7 | import Phoenix.Channel as Channel 8 | import Phoenix.Event as Event exposing (Event) 9 | import Phoenix.Internal.Message as InternalMessage exposing (InternalMessage(..)) 10 | import Phoenix.Internal.Socket as InternalSocket 11 | import Phoenix.Message as Message 12 | import Phoenix.Push as Push 13 | import Test exposing (..) 14 | 15 | 16 | suite : Test 17 | suite = 18 | describe "internal socket" 19 | [ describe "Handle internal socket messages" 20 | [ test "parse a direct event from server" <| 21 | \_ -> 22 | let 23 | channel = 24 | Channel.init "numbers:positive" 25 | |> Channel.on "update" UpdateEvent 26 | 27 | eventPayload = 28 | Encode.object [ ( "number", Encode.int 802 ) ] 29 | 30 | event = 31 | Event.init "update" "numbers:positive" eventPayload Nothing 32 | 33 | channels = 34 | Channel.addChannel channel Dict.empty 35 | 36 | msg = 37 | event 38 | |> InternalSocket.mapExternalEvents Dict.empty channels 39 | |> Message.extractExternalMsg 40 | in 41 | Expect.equal msg (Just (UpdateEvent eventPayload)) 42 | , test "parse successful join channel reply" <| 43 | \_ -> 44 | let 45 | joinRef = 46 | 10 47 | 48 | channel = 49 | Channel.init "numbers:positive" 50 | |> Channel.onJoin JoinedChannel 51 | |> Channel.setJoiningState joinRef 52 | 53 | eventPayload = 54 | Encode.object [ ( "initial", Encode.int 0 ) ] 55 | 56 | status = 57 | Encode.object [ ( "status", Encode.string "ok" ), ( "response", eventPayload ) ] 58 | 59 | event = 60 | Event.init "phx_reply" "numbers:positive" status (Just joinRef) 61 | 62 | channels = 63 | Channel.addChannel channel Dict.empty 64 | 65 | msg = 66 | event 67 | |> InternalSocket.mapExternalEvents Dict.empty channels 68 | |> Message.extractExternalMsg 69 | in 70 | Expect.equal msg (Just (JoinedChannel eventPayload)) 71 | , test "parse unsuccessful join channel reply" <| 72 | \_ -> 73 | let 74 | joinRef = 75 | 10 76 | 77 | channel = 78 | Channel.init "numbers:positive" 79 | |> Channel.onJoinError JoinedChannel 80 | |> Channel.setJoiningState joinRef 81 | 82 | eventPayload = 83 | Encode.object [ ( "initial", Encode.int 20 ) ] 84 | 85 | status = 86 | Encode.object [ ( "status", Encode.string "error" ), ( "response", eventPayload ) ] 87 | 88 | event = 89 | Event.init "phx_reply" "numbers:positive" status (Just joinRef) 90 | 91 | channels = 92 | Channel.addChannel channel Dict.empty 93 | 94 | msg = 95 | event 96 | |> InternalSocket.mapExternalEvents Dict.empty channels 97 | |> Message.extractExternalMsg 98 | in 99 | Expect.equal msg (Just (JoinedChannel eventPayload)) 100 | , test "parse error event" <| 101 | \_ -> 102 | let 103 | channel = 104 | Channel.init "numbers:positive" 105 | |> Channel.onError OnChannelError 106 | 107 | eventPayload = 108 | Encode.object [ ( "error", Encode.int 402 ) ] 109 | 110 | event = 111 | Event.init "phx_error" "numbers:positive" eventPayload (Just 192) 112 | 113 | channels = 114 | Channel.addChannel channel Dict.empty 115 | 116 | msg = 117 | event 118 | |> InternalSocket.mapExternalEvents Dict.empty channels 119 | |> Message.extractExternalMsg 120 | in 121 | Expect.equal msg (Just (OnChannelError eventPayload)) 122 | , test "parse closed event" <| 123 | \_ -> 124 | let 125 | channel = 126 | Channel.init "numbers:positive" 127 | |> Channel.onClose OnChannelClosed 128 | 129 | eventPayload = 130 | Encode.object [ ( "error", Encode.string "closed" ) ] 131 | 132 | event = 133 | Event.init "phx_close" "numbers:positive" eventPayload (Just 2) 134 | 135 | channels = 136 | Channel.addChannel channel Dict.empty 137 | 138 | msg = 139 | event 140 | |> InternalSocket.mapExternalEvents Dict.empty channels 141 | |> Message.extractExternalMsg 142 | in 143 | Expect.equal msg (Just (OnChannelClosed eventPayload)) 144 | , test "parse an ok reply to a push event" <| 145 | \_ -> 146 | let 147 | joinRef = 148 | 16 149 | 150 | channel = 151 | Channel.init "numbers:positive" 152 | 153 | push = 154 | Push.initWithChannel "get_current_value" channel 155 | |> Push.onOk OnPushedOk 156 | 157 | eventPayload = 158 | Encode.object [ ( "current_value", Encode.int 28 ) ] 159 | 160 | status = 161 | Encode.object [ ( "status", Encode.string "ok" ), ( "response", eventPayload ) ] 162 | 163 | event = 164 | Event.init "phx_reply" "numbers:positive" status (Just joinRef) 165 | 166 | channels = 167 | Channel.addChannel channel Dict.empty 168 | 169 | pushedEvents = 170 | Dict.fromList [ ( joinRef, push ) ] 171 | 172 | msg = 173 | event 174 | |> InternalSocket.mapExternalEvents pushedEvents channels 175 | |> Message.extractExternalMsg 176 | in 177 | Expect.equal msg (Just (OnPushedOk eventPayload)) 178 | , test "parse an error reply to a push event" <| 179 | \_ -> 180 | let 181 | joinRef = 182 | 16 183 | 184 | channel = 185 | Channel.init "numbers:positive" 186 | 187 | push = 188 | Push.initWithChannel "get_current_value" channel 189 | |> Push.onError OnPushedError 190 | 191 | eventPayload = 192 | Encode.object [ ( "current_value", Encode.int 28 ) ] 193 | 194 | status = 195 | Encode.object [ ( "status", Encode.string "error" ), ( "response", eventPayload ) ] 196 | 197 | event = 198 | Event.init "phx_reply" "numbers:positive" status (Just joinRef) 199 | 200 | channels = 201 | Channel.addChannel channel Dict.empty 202 | 203 | pushedEvents = 204 | Dict.fromList [ ( joinRef, push ) ] 205 | 206 | msg = 207 | event 208 | |> InternalSocket.mapExternalEvents pushedEvents channels 209 | |> Message.extractExternalMsg 210 | in 211 | Expect.equal msg (Just (OnPushedError eventPayload)) 212 | ] 213 | ] 214 | 215 | 216 | type TestMsg 217 | = UpdateEvent Encode.Value 218 | | JoinedChannel Encode.Value 219 | | OnChannelError Encode.Value 220 | | OnChannelClosed Encode.Value 221 | | OnPushedOk Encode.Value 222 | | OnPushedError Encode.Value 223 | -------------------------------------------------------------------------------- /tests/Phoenix/ChannelTest.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.ChannelTest exposing (TestMsg(..), suite) 2 | 3 | import Dict 4 | import Expect exposing (Expectation) 5 | import Fuzz exposing (Fuzzer, int, list, string) 6 | import Json.Decode as Decode exposing (Value) 7 | import Phoenix.Channel as Channel 8 | import Test exposing (..) 9 | 10 | 11 | suite : Test 12 | suite = 13 | describe "initialise channel" 14 | [ describe "Socket.init" 15 | [ fuzz Fuzz.string "initialize channel" <| 16 | \topic -> 17 | let 18 | channel = 19 | Channel.init topic 20 | in 21 | Expect.equal (Channel.topic channel) topic 22 | ] 23 | , describe 24 | "Channel state" 25 | [ fuzz2 Fuzz.string Fuzz.int "set joining ref" <| 26 | \topic joinRef -> 27 | let 28 | channel = 29 | topic 30 | |> Channel.init 31 | |> Channel.setJoiningState joinRef 32 | in 33 | Expect.equal (Channel.joinRef channel) (Just joinRef) 34 | 35 | -- , fuzz2 Fuzz.string Fuzz.int "set joining state" <| 36 | -- \topic joinRef -> 37 | -- let 38 | -- channel = 39 | -- Channel.init topic 40 | -- 41 | -- channelJoining = 42 | -- Channel.setJoiningState joinRef channel 43 | -- in 44 | -- Expect.notEqual channel.state channelJoining.state 45 | -- , fuzz2 Fuzz.string Fuzz.int "set joined state" <| 46 | -- \topic joinRef -> 47 | -- let 48 | -- channel = 49 | -- topic 50 | -- |> Channel.init 51 | -- |> Channel.setJoiningState joinRef 52 | -- 53 | -- channelJoined = 54 | -- Channel.setJoinedState channel 55 | -- in 56 | -- Expect.notEqual channel.state channelJoined.state 57 | , fuzz2 Fuzz.string Fuzz.int "new Channel state should not be on going" <| 58 | \topic joinRef -> 59 | let 60 | channel = 61 | topic 62 | |> Channel.init 63 | in 64 | Expect.equal (Channel.isOngoing channel) False 65 | , fuzz2 Fuzz.string Fuzz.int "Channel should be on going after setJoiningState is being called" <| 66 | \topic joinRef -> 67 | let 68 | channel = 69 | topic 70 | |> Channel.init 71 | |> Channel.setJoiningState joinRef 72 | in 73 | Expect.equal (Channel.isOngoing channel) True 74 | ] 75 | , describe "Find a channel in Dict" 76 | [ fuzz Fuzz.string "find a channel in an empty Dict" <| 77 | \topic -> 78 | let 79 | channel = 80 | Channel.init topic 81 | in 82 | Expect.equal (Channel.findChannel (Channel.topic channel) Dict.empty) Nothing 83 | , test "find a channel that is not in channel Dict" <| 84 | \_ -> 85 | let 86 | channel = 87 | Channel.init "mytopic" 88 | 89 | channelDict = 90 | Channel.addChannel channel Dict.empty 91 | in 92 | Expect.equal (Channel.findChannel "othertopic" channelDict) Nothing 93 | , fuzz Fuzz.string "find a channel in a Dict by its topic" <| 94 | \topic -> 95 | let 96 | channel = 97 | Channel.init topic 98 | 99 | channelDict = 100 | Channel.addChannel channel Dict.empty 101 | in 102 | Expect.equal (Channel.findChannel (Channel.topic channel) channelDict) (Just channel) 103 | , fuzz2 Fuzz.string Fuzz.int "find a channel in a Dict by its topic and joinRef" <| 104 | \topic joinRef -> 105 | let 106 | channel = 107 | topic 108 | |> Channel.init 109 | |> Channel.setJoiningState joinRef 110 | 111 | channelDict = 112 | Channel.addChannel channel Dict.empty 113 | in 114 | Expect.equal (Channel.findChannelWithRef (Channel.topic channel) (Just joinRef) channelDict) (Just channel) 115 | , fuzz Fuzz.string "find a channel in a Dict by its topic and wrong joinRef" <| 116 | \topic -> 117 | let 118 | joinRef = 119 | 1 120 | 121 | channel = 122 | topic 123 | |> Channel.init 124 | |> Channel.setJoiningState joinRef 125 | 126 | channelDict = 127 | Channel.addChannel channel Dict.empty 128 | in 129 | Expect.equal (Channel.findChannelWithRef (Channel.topic channel) (Just 2) channelDict) Nothing 130 | , test "find a channel in a Dict by wrong topic and wrong joinRef" <| 131 | \_ -> 132 | let 133 | joinRef = 134 | 1 135 | 136 | topic = 137 | "chat:myroom" 138 | 139 | channel = 140 | topic 141 | |> Channel.init 142 | |> Channel.setJoiningState joinRef 143 | 144 | channelDict = 145 | Channel.addChannel channel Dict.empty 146 | in 147 | Expect.equal (Channel.findChannelWithRef "foo" (Just 2) channelDict) Nothing 148 | ] 149 | , describe "update a channel in channel dic" 150 | [ test "update a channel" <| 151 | \_ -> 152 | let 153 | channel = 154 | Channel.init "chat:rootm1" 155 | 156 | channels = 157 | Channel.addChannel channel Dict.empty 158 | in 159 | Expect.equal (Channel.updateChannel channel channels) channels 160 | ] 161 | 162 | -- , describe "triggers messages" 163 | -- [ test "new channel has 0 receive trigger messages" <| 164 | -- \_ -> 165 | -- Expect.equal (Dict.size (Channel.init "chan").receive) 0 166 | -- , test "onJoin" <| 167 | -- \_ -> 168 | -- let 169 | -- channel = 170 | -- "chat:rootm1" 171 | -- |> Channel.init 172 | -- |> Channel.onJoin AppMessage 173 | -- in 174 | -- Expect.equal (Dict.get "ok" channel.receive) (Just AppMessage) 175 | -- , test "onJoinError" <| 176 | -- \_ -> 177 | -- let 178 | -- channel = 179 | -- "chat:rootm1" 180 | -- |> Channel.init 181 | -- |> Channel.onJoinError AppMessage 182 | -- in 183 | -- Expect.equal (Dict.get "join_error" channel.receive) (Just AppMessage) 184 | -- , test "onError" <| 185 | -- \_ -> 186 | -- let 187 | -- channel = 188 | -- "chat:rootm1" 189 | -- |> Channel.init 190 | -- |> Channel.onError AppMessage 191 | -- in 192 | -- Expect.equal (Dict.get "error" channel.receive) (Just AppMessage) 193 | -- , test "onClose" <| 194 | -- \_ -> 195 | -- let 196 | -- channel = 197 | -- "chat:rootm1" 198 | -- |> Channel.init 199 | -- |> Channel.onClose AppMessage 200 | -- in 201 | -- Expect.equal (Dict.get "close" channel.receive) (Just AppMessage) 202 | -- , test "on" <| 203 | -- \_ -> 204 | -- let 205 | -- channel = 206 | -- "chat:rootm1" 207 | -- |> Channel.init 208 | -- |> Channel.on "foo_event" AppMessage 209 | -- in 210 | -- Expect.equal (Dict.get "foo_event" channel.on) (Just AppMessage) 211 | -- ] 212 | ] 213 | 214 | 215 | type TestMsg 216 | = AppMessage Value 217 | -------------------------------------------------------------------------------- /src/Phoenix/Socket.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.Socket exposing (Socket, init, update, join, listen, push, withLongPoll, subscribe, endPoint, pushedEvents, channels, heartbeatTimestamp, withPayload) 2 | 3 | {-| 4 | 5 | 6 | # This module provides an interface for connecting to Phoenix Socket 7 | 8 | @docs Socket, init, update, join, listen, push, withLongPoll, subscribe, endPoint, pushedEvents, channels, heartbeatTimestamp, withPayload 9 | 10 | -} 11 | 12 | import Dict exposing (Dict) 13 | import Json.Encode as Encode 14 | import Phoenix.Channel as Channel exposing (Channel) 15 | import Phoenix.Event as Event exposing (Event) 16 | import Phoenix.Internal.LongPoll as LongPoll 17 | import Phoenix.Internal.Message as InternalMessage exposing (InternalMessage(..)) 18 | import Phoenix.Message as Message exposing (Msg) 19 | import Phoenix.Push as Push exposing (Push) 20 | import Phoenix.Serializer exposing (Serializer(..)) 21 | import Regex 22 | import Time 23 | 24 | 25 | second = 26 | 1000 27 | 28 | 29 | type Transport 30 | = WebSocket 31 | | LongPoll 32 | 33 | 34 | type State 35 | = Connecting 36 | | Open 37 | | Closing 38 | | Closed 39 | 40 | 41 | {-| Socket model 42 | -} 43 | type Socket msg 44 | = Socket 45 | { endPoint : String 46 | , channels : Dict String (Channel msg) 47 | , serializer : Serializer 48 | , transport : Transport 49 | , pushedEvents : Dict Int (Push msg) 50 | , heartbeatIntervalSeconds : Float 51 | , heartbeatTimestamp : Time.Posix 52 | , heartbeatReplyTimestamp : Time.Posix 53 | , longPollToken : Maybe String 54 | , ref : Int 55 | , readyState : State 56 | , withDebug : Bool 57 | , withoutHeartbeat : Bool 58 | , payload : List ( String, String ) 59 | } 60 | 61 | 62 | {-| Initializes Socket using the websocket address 63 | -} 64 | init : String -> Socket msg 65 | init endPointStr = 66 | Socket 67 | { endPoint = websocketEndPointToLongPollEndPoint endPointStr 68 | , channels = Dict.empty 69 | , serializer = V1 70 | , transport = LongPoll 71 | , pushedEvents = Dict.empty 72 | , heartbeatIntervalSeconds = 30 73 | , heartbeatTimestamp = Time.millisToPosix 0 74 | , heartbeatReplyTimestamp = Time.millisToPosix 0 75 | , ref = 1 76 | , longPollToken = Nothing 77 | , readyState = Closed 78 | , withDebug = False 79 | , withoutHeartbeat = False 80 | , payload = [] 81 | } 82 | 83 | 84 | {-| withLongPoll 85 | -} 86 | withLongPoll : Socket msg -> Socket msg 87 | withLongPoll (Socket socket) = 88 | Socket 89 | { socket 90 | | transport = LongPoll 91 | , endPoint = websocketEndPointToLongPollEndPoint socket.endPoint 92 | } 93 | 94 | 95 | {-| withPayload 96 | -} 97 | withPayload : List ( String, String ) -> Socket msg -> Socket msg 98 | withPayload payload (Socket socket) = 99 | Socket 100 | { socket | payload = payload } 101 | 102 | 103 | {-| Joins a channel 104 | Adds the channel to the socket model and returns a command that sends data 105 | -} 106 | join : Channel msg -> Socket msg -> ( Socket msg, Cmd (Msg msg) ) 107 | join channel (Socket socket) = 108 | case Dict.get (Channel.topic channel) socket.channels of 109 | Just channelItm -> 110 | if Channel.isOngoing channelItm then 111 | ( Socket socket, Cmd.none ) 112 | 113 | else 114 | doJoin channel (Socket socket) 115 | 116 | Nothing -> 117 | doJoin channel (Socket socket) 118 | 119 | 120 | {-| Subscribes to a channel events 121 | Adds the channel to the socket model 122 | -} 123 | subscribe : Channel msg -> Socket msg -> ( Socket msg, Cmd (Msg msg) ) 124 | subscribe channel socket = 125 | ( addChannel (Channel.setJoinedState channel) socket, Cmd.none ) 126 | 127 | 128 | {-| Listens to socket 129 | -} 130 | listen : (Msg msg -> msg) -> Socket msg -> Sub msg 131 | listen toExternalAppMsgFn (Socket socket) = 132 | let 133 | freq = 134 | case socket.readyState of 135 | Closed -> 136 | second 137 | 138 | Open -> 139 | 5 * second 140 | 141 | _ -> 142 | 10 * second 143 | in 144 | Time.every freq LongPollTick 145 | |> Sub.map Message.toInternalMsg 146 | |> Sub.map toExternalAppMsgFn 147 | 148 | 149 | {-| Handles Phoenix Msg 150 | -} 151 | update : (Msg msg -> msg) -> Msg msg -> Socket msg -> ( Socket msg, Cmd msg ) 152 | update toExternalAppMsgFn msg (Socket socket) = 153 | case Message.extractInternalMsg msg of 154 | ChannelSuccessfullyJoined channel response -> 155 | let 156 | updatedChannel = 157 | Channel.setJoinedState channel 158 | 159 | updateSocket = 160 | Socket { socket | channels = Channel.updateChannel updatedChannel socket.channels } 161 | in 162 | ( updateSocket, Cmd.none ) 163 | 164 | ChannelFailedToJoin channel response -> 165 | let 166 | updatedChannel = 167 | Channel.setErroredState channel 168 | 169 | updateSocket = 170 | Socket { socket | channels = Channel.updateChannel updatedChannel socket.channels } 171 | in 172 | ( updateSocket, Cmd.none ) 173 | 174 | ChannelError channel response -> 175 | let 176 | updatedChannel = 177 | Channel.setErroredState channel 178 | 179 | updateSocket = 180 | Socket { socket | channels = Channel.updateChannel updatedChannel socket.channels } 181 | in 182 | ( updateSocket, Cmd.none ) 183 | 184 | ChannelClosed channel response -> 185 | let 186 | updatedChannel = 187 | Channel.setClosedState channel 188 | 189 | updateSocket = 190 | Socket { socket | channels = Channel.updateChannel updatedChannel socket.channels } 191 | in 192 | ( updateSocket, Cmd.none ) 193 | 194 | Heartbeat heartbeatTimestampVal -> 195 | let 196 | ( updateSocket, cmd ) = 197 | heartbeat (Socket { socket | heartbeatTimestamp = heartbeatTimestampVal }) 198 | in 199 | ( updateSocket, Cmd.map toExternalAppMsgFn cmd ) 200 | 201 | HeartbeatReply -> 202 | ( Socket { socket | heartbeatReplyTimestamp = socket.heartbeatTimestamp }, Cmd.none ) 203 | 204 | LongPollTick _ -> 205 | case socket.readyState of 206 | Open -> 207 | ( Socket { socket | readyState = Connecting }, Cmd.map toExternalAppMsgFn (LongPoll.poll socket.endPoint socket.payload socket.longPollToken) ) 208 | 209 | Connecting -> 210 | ( Socket socket, Cmd.none ) 211 | 212 | Closing -> 213 | ( Socket socket, Cmd.none ) 214 | 215 | Closed -> 216 | ( Socket { socket | readyState = Connecting }, Cmd.map toExternalAppMsgFn (LongPoll.poll socket.endPoint socket.payload socket.longPollToken) ) 217 | 218 | LongPollPolled (Ok longPollEvent) -> 219 | case longPollEvent.status of 220 | 200 -> 221 | let 222 | command = 223 | case longPollEvent.messages of 224 | Just [] -> 225 | Cmd.none 226 | 227 | Nothing -> 228 | Cmd.none 229 | 230 | Just messsages -> 231 | LongPoll.externalMsgs socket.pushedEvents socket.channels toExternalAppMsgFn messsages 232 | in 233 | ( Socket { socket | readyState = Closed }, command ) 234 | 235 | 401 -> 236 | -- TODO: send onJoinError Msg 237 | ( Socket socket, Cmd.none ) 238 | 239 | 410 -> 240 | -- connecten is opened 241 | ( Socket { socket | readyState = Open, longPollToken = longPollEvent.token }, Cmd.none ) 242 | 243 | 204 -> 244 | -- no content 245 | ( Socket { socket | readyState = Open }, Cmd.none ) 246 | 247 | _ -> 248 | ( Socket { socket | readyState = Closed }, Cmd.none ) 249 | 250 | LongPollPolled (Err _) -> 251 | ( Socket { socket | readyState = Closed }, Cmd.none ) 252 | 253 | LongPollSent (Err _) -> 254 | ( Socket socket, Cmd.none ) 255 | 256 | LongPollSent (Ok _) -> 257 | ( Socket socket, Cmd.none ) 258 | 259 | NoOp -> 260 | ( Socket socket, Cmd.none ) 261 | 262 | 263 | {-| pushs a message 264 | -} 265 | push : Push msg -> Socket msg -> ( Socket msg, Cmd (Msg msg) ) 266 | push pushRecord (Socket socket) = 267 | let 268 | event = 269 | Event pushRecord.event pushRecord.topic pushRecord.payload (Just socket.ref) 270 | in 271 | doPush event (Just pushRecord) (Socket socket) 272 | 273 | 274 | doPush : Event -> Maybe (Push msg) -> Socket msg -> ( Socket msg, Cmd (Msg msg) ) 275 | doPush event maybePush (Socket socket) = 276 | let 277 | socketType = 278 | addPushedEvent maybePush (Socket socket) 279 | in 280 | ( socketType, LongPoll.send socket.endPoint socket.longPollToken event ) 281 | 282 | 283 | doJoin : Channel msg -> Socket msg -> ( Socket msg, Cmd (Msg msg) ) 284 | doJoin channel (Socket socket) = 285 | let 286 | eventName = 287 | "phx_join" 288 | 289 | updatedChannel = 290 | Channel.setJoiningState socket.ref channel 291 | 292 | event = 293 | Event.init eventName (Channel.topic channel) (Channel.payload channel) (Just socket.ref) 294 | 295 | updateSocket = 296 | Socket socket 297 | |> addPushedEvent Nothing 298 | |> addChannel updatedChannel 299 | in 300 | ( updateSocket, LongPoll.send socket.endPoint socket.longPollToken event ) 301 | 302 | 303 | addPushedEvent : Maybe (Push msg) -> Socket msg -> Socket msg 304 | addPushedEvent maybePush (Socket socket) = 305 | let 306 | pushedEventsVal = 307 | case maybePush of 308 | Just pushVal -> 309 | case Dict.size pushVal.on of 310 | 0 -> 311 | socket.pushedEvents 312 | 313 | _ -> 314 | Dict.insert socket.ref pushVal socket.pushedEvents 315 | 316 | Nothing -> 317 | socket.pushedEvents 318 | in 319 | Socket { socket | pushedEvents = pushedEventsVal, ref = socket.ref + 1 } 320 | 321 | 322 | addChannel : Channel msg -> Socket msg -> Socket msg 323 | addChannel channel (Socket socket) = 324 | Socket { socket | channels = Channel.addChannel channel socket.channels } 325 | 326 | 327 | heartbeat : Socket msg -> ( Socket msg, Cmd (Msg msg) ) 328 | heartbeat (Socket socket) = 329 | let 330 | event = 331 | Event.init "heartbeat" "phoenix" (Encode.object []) (Just socket.ref) 332 | in 333 | doPush event Nothing (Socket socket) 334 | 335 | 336 | {-| -} 337 | endPoint : Socket msg -> String 338 | endPoint socketType = 339 | case socketType of 340 | Socket socket -> 341 | socket.endPoint 342 | 343 | 344 | {-| -} 345 | pushedEvents : Socket msg -> Dict Int (Push msg) 346 | pushedEvents socketType = 347 | case socketType of 348 | Socket socket -> 349 | socket.pushedEvents 350 | 351 | 352 | {-| -} 353 | channels : Socket msg -> Dict String (Channel msg) 354 | channels socketType = 355 | case socketType of 356 | Socket socket -> 357 | socket.channels 358 | 359 | 360 | {-| -} 361 | heartbeatTimestamp : Socket msg -> Time.Posix 362 | heartbeatTimestamp socketType = 363 | case socketType of 364 | Socket socket -> 365 | socket.heartbeatTimestamp 366 | 367 | 368 | websocketEndPointToLongPollEndPoint : String -> String 369 | websocketEndPointToLongPollEndPoint endPointVal = 370 | let 371 | websocketRouteRegex = 372 | Maybe.withDefault Regex.never <| Regex.fromString "/websocket$" 373 | 374 | wsRegex = 375 | Maybe.withDefault Regex.never <| Regex.fromString "^ws://" 376 | 377 | wssRegex = 378 | Maybe.withDefault Regex.never <| Regex.fromString "^wss://" 379 | in 380 | endPointVal 381 | |> Regex.replace websocketRouteRegex (\_ -> "/longpoll") 382 | |> Regex.replace wsRegex (\_ -> "http://") 383 | |> Regex.replace wssRegex (\_ -> "https://") 384 | -------------------------------------------------------------------------------- /tests/Phoenix/SocketTest.elm: -------------------------------------------------------------------------------- 1 | module Phoenix.SocketTest exposing (TestMsg(..), basicEndpoint, endPointFuzzer, suite) 2 | 3 | import Dict 4 | import Expect exposing (Expectation) 5 | import Fuzz exposing (Fuzzer, int, list, string) 6 | import Json.Decode as Decode exposing (Value) 7 | import Json.Encode as Encode 8 | import Phoenix.Channel as Channel 9 | import Phoenix.Internal.Message as InternalMessage exposing (InternalMessage(..)) 10 | import Phoenix.Message as Message 11 | import Phoenix.Push as Push 12 | import Phoenix.Socket as Socket 13 | import Test exposing (..) 14 | import Time 15 | 16 | 17 | basicEndpoint = 18 | "ws://localhost:4000/socket/websocket" 19 | 20 | 21 | suite : Test 22 | suite = 23 | describe "initialise socket" 24 | [ describe "Socket.init" 25 | [ fuzz endPointFuzzer "initialize Socket" <| 26 | \endPoint -> 27 | let 28 | socket = 29 | Socket.init endPoint 30 | in 31 | Expect.equal (Socket.endPoint socket) endPoint 32 | ] 33 | , describe "initialise socket with channel" 34 | [ test "default init should return empty channel list" <| 35 | \_ -> 36 | let 37 | socket = 38 | Socket.init basicEndpoint 39 | in 40 | Expect.equal (Dict.size (Socket.channels socket)) 0 41 | , test "join should add an item to list of channels" <| 42 | \_ -> 43 | let 44 | channel = 45 | Channel.init "chat:room233" 46 | 47 | ( socket, cmd ) = 48 | basicEndpoint 49 | |> Socket.init 50 | |> Socket.join channel 51 | in 52 | Expect.equal (Dict.size (Socket.channels socket)) 1 53 | , test "subscribe should add an item to list of channels" <| 54 | \_ -> 55 | let 56 | channel = 57 | Channel.init "chat:room233" 58 | 59 | ( socket, cmd ) = 60 | basicEndpoint 61 | |> Socket.init 62 | |> Socket.subscribe channel 63 | in 64 | Expect.equal (Dict.size (Socket.channels socket)) 1 65 | , test "calling join again on a channel that is not joined yet should be ignored" <| 66 | \_ -> 67 | let 68 | channel = 69 | Channel.init "chat:room233" 70 | 71 | ( socket, cmd ) = 72 | basicEndpoint 73 | |> Socket.init 74 | |> Socket.join channel 75 | |> Tuple.first 76 | |> Socket.join channel 77 | in 78 | Expect.equal (Dict.size (Socket.channels socket)) 1 79 | , test "join should add an item to list of pushedEvents" <| 80 | \_ -> 81 | let 82 | channel = 83 | Channel.init "chat:room233" 84 | 85 | ( socket, cmd ) = 86 | basicEndpoint 87 | |> Socket.init 88 | |> Socket.join channel 89 | in 90 | case Dict.get (Channel.topic channel) (Socket.channels socket) of 91 | Just ch -> 92 | Expect.equal (Channel.isOngoing ch) True 93 | 94 | _ -> 95 | Expect.fail "not possible!" 96 | ] 97 | , describe "listen to socket" 98 | [ test "return subscription" <| 99 | \_ -> 100 | let 101 | socket = 102 | Socket.init basicEndpoint 103 | 104 | sub = 105 | Socket.listen PhoenixMsg socket 106 | in 107 | Expect.notEqual sub Sub.none 108 | ] 109 | , describe "updates socket model" 110 | [ test "none" <| 111 | \_ -> 112 | let 113 | socket = 114 | basicEndpoint 115 | |> Socket.init 116 | 117 | msg = 118 | Message.toInternalMsg NoOp 119 | 120 | ( updatedSocket, cmd ) = 121 | Socket.update PhoenixMsg msg socket 122 | in 123 | Expect.equal cmd Cmd.none 124 | , test "ChannelSuccessfullyJoined" <| 125 | \_ -> 126 | let 127 | channel = 128 | "chat:room233" 129 | |> Channel.init 130 | |> Channel.onJoin JoinedChannel 131 | 132 | value = 133 | Encode.object [] 134 | 135 | ( socket, _ ) = 136 | basicEndpoint 137 | |> Socket.init 138 | |> Socket.join channel 139 | 140 | msg = 141 | Message.toInternalMsg (ChannelSuccessfullyJoined channel value) 142 | 143 | ( updatedSocket, _ ) = 144 | Socket.update PhoenixMsg msg socket 145 | 146 | joinedChannel = 147 | Channel.findChannel (Channel.topic channel) (Socket.channels updatedSocket) 148 | in 149 | case joinedChannel of 150 | Just jc -> 151 | Expect.equal (Channel.isJoined jc) True 152 | 153 | _ -> 154 | Expect.fail "couldn't find the channel!" 155 | , test "ChannelFailedToJoin" <| 156 | \_ -> 157 | let 158 | channel = 159 | "chat:room233" 160 | |> Channel.init 161 | |> Channel.onJoinError FailedToJoinedChannel 162 | 163 | value = 164 | Encode.object [] 165 | 166 | ( socket, _ ) = 167 | basicEndpoint 168 | |> Socket.init 169 | |> Socket.join channel 170 | 171 | msg = 172 | Message.toInternalMsg (ChannelFailedToJoin channel value) 173 | 174 | ( updatedSocket, _ ) = 175 | Socket.update PhoenixMsg msg socket 176 | 177 | errChannel = 178 | Channel.findChannel (Channel.topic channel) (Socket.channels updatedSocket) 179 | in 180 | case errChannel of 181 | Just ec -> 182 | Expect.equal (Channel.isErrored ec) True 183 | 184 | _ -> 185 | Expect.fail "couldn't find the channel!" 186 | , test "ChannelError when receive event phx_error with topic of channel" <| 187 | \_ -> 188 | let 189 | channel = 190 | "chat:room233" 191 | |> Channel.init 192 | |> Channel.onError ChannelGotError 193 | 194 | value = 195 | Encode.object [] 196 | 197 | ( socket, _ ) = 198 | basicEndpoint 199 | |> Socket.init 200 | |> Socket.join channel 201 | 202 | msg = 203 | Message.toInternalMsg (ChannelError channel value) 204 | 205 | ( updatedSocket, _ ) = 206 | Socket.update PhoenixMsg msg socket 207 | 208 | errChannel = 209 | Channel.findChannel (Channel.topic channel) (Socket.channels updatedSocket) 210 | in 211 | case errChannel of 212 | Just ec -> 213 | Expect.equal (Channel.isErrored ec) True 214 | 215 | _ -> 216 | Expect.fail "couldn't find the channel!" 217 | , test "ChannelClosed when server closes channel" <| 218 | \_ -> 219 | let 220 | channel = 221 | "chat:room233" 222 | |> Channel.init 223 | |> Channel.onClose ChannelGotClosed 224 | 225 | value = 226 | Encode.object [] 227 | 228 | ( socket, _ ) = 229 | basicEndpoint 230 | |> Socket.init 231 | |> Socket.join channel 232 | 233 | msg = 234 | Message.toInternalMsg (ChannelClosed channel value) 235 | 236 | ( updatedSocket, _ ) = 237 | Socket.update PhoenixMsg msg socket 238 | 239 | errChannel = 240 | Channel.findChannel (Channel.topic channel) (Socket.channels updatedSocket) 241 | in 242 | case errChannel of 243 | Just ec -> 244 | Expect.equal (Channel.isClosed ec) True 245 | 246 | _ -> 247 | Expect.fail "couldn't find the channel!" 248 | , test "Heartbeat" <| 249 | \_ -> 250 | let 251 | channel = 252 | "chat:room233" 253 | |> Channel.init 254 | 255 | value = 256 | Encode.object [] 257 | 258 | msg = 259 | Message.toInternalMsg (Heartbeat (Time.millisToPosix 19292922)) 260 | 261 | socket = 262 | basicEndpoint 263 | |> Socket.init 264 | |> Socket.join channel 265 | |> Tuple.first 266 | |> Socket.update PhoenixMsg msg 267 | |> Tuple.first 268 | in 269 | Expect.equal (Socket.heartbeatTimestamp socket) (Time.millisToPosix 19292922) 270 | ] 271 | , describe "pushs event" 272 | [ test "push an event" <| 273 | \_ -> 274 | let 275 | channel = 276 | "chat:room233" 277 | |> Channel.init 278 | 279 | payload = 280 | Encode.object [ ( "name", Encode.string "foo" ) ] 281 | 282 | push = 283 | Push.initWithChannel "hello" channel 284 | |> Push.withPayload payload 285 | |> Push.onOk OnOkCmd 286 | 287 | socket = 288 | basicEndpoint 289 | |> Socket.init 290 | |> Socket.push push 291 | |> Tuple.first 292 | in 293 | case Dict.get 1 (Socket.pushedEvents socket) of 294 | Just event -> 295 | Expect.equal event.payload payload 296 | 297 | _ -> 298 | Expect.fail "couldn't find pushed event" 299 | , test "push second event" <| 300 | \_ -> 301 | let 302 | channel = 303 | "chat:room233" 304 | |> Channel.init 305 | 306 | payload = 307 | Encode.object [ ( "name", Encode.string "foo" ) ] 308 | 309 | push = 310 | Push.initWithChannel "hello" channel 311 | |> Push.withPayload payload 312 | |> Push.onOk OnOkCmd 313 | 314 | socket = 315 | basicEndpoint 316 | |> Socket.init 317 | |> Socket.push push 318 | |> Tuple.first 319 | |> Socket.push push 320 | |> Tuple.first 321 | in 322 | case Dict.get 2 (Socket.pushedEvents socket) of 323 | Just event -> 324 | Expect.equal event.payload payload 325 | 326 | _ -> 327 | Expect.fail "couldn't find second pushed event" 328 | ] 329 | ] 330 | 331 | 332 | type TestMsg 333 | = PhoenixMsg (Message.Msg TestMsg) 334 | | JoinedChannel Value 335 | | FailedToJoinedChannel Value 336 | | ChannelGotError Value 337 | | ChannelGotClosed Value 338 | | OnOkCmd Value 339 | 340 | 341 | endPointFuzzer : Fuzzer String 342 | endPointFuzzer = 343 | Fuzz.string 344 | |> Fuzz.map (\s -> "ws://" ++ s) 345 | --------------------------------------------------------------------------------