halogen-websocket-ipc-playzone/src/App/WS.purs

315 lines
10 KiB
Plaintext
Raw Normal View History

2023-07-03 18:03:47 +02:00
module App.WS where
{- This component handles all WS operations. -}
2023-07-05 04:49:32 +02:00
import Prelude (Unit, bind, discard, pure, show, void, when, ($), (&&), (<$>), (<>), (>>=), (>=>), (<<<), map)
2023-07-03 18:03:47 +02:00
2023-07-05 04:49:32 +02:00
import Control.Monad.Except (runExcept)
import Data.Array as A
import Data.ArrayBuffer.Types (ArrayBuffer)
import Data.Bifunctor (lmap)
import Data.Either (Either(..))
2023-07-03 18:03:47 +02:00
import Data.Maybe (Maybe(..), isJust, isNothing)
import Data.String as String
import Data.Tuple (Tuple(..))
import Effect.Aff.Class (class MonadAff)
2023-07-05 04:49:32 +02:00
import Effect (Effect)
import Foreign as F
2023-07-03 18:03:47 +02:00
import Halogen as H
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.HTML.Properties as HP
2023-07-05 04:49:32 +02:00
import Halogen.Query.Event as HQE
import Halogen.Subscription as HS
import Web.Socket.BinaryType (BinaryType(ArrayBuffer))
import Web.Socket.Event.CloseEvent as WSCE
import Web.Socket.Event.EventTypes as WSET
import Web.Socket.Event.MessageEvent as WSME
2023-07-03 18:03:47 +02:00
import Web.Socket.ReadyState (ReadyState(Connecting, Open, Closing, Closed))
2023-07-05 04:49:32 +02:00
import Web.Socket.WebSocket as WS
2023-07-03 18:03:47 +02:00
2023-07-05 04:49:32 +02:00
import App.LogMessage
2023-07-03 18:03:47 +02:00
-- Input is the WS url.
2023-07-03 18:03:47 +02:00
type Input = String
2023-07-03 18:03:47 +02:00
-- MessageReceived (Tuple URL message)
data Output
= MessageReceived (Tuple String ArrayBuffer) -- Provide a received message to the parent.
| WSJustConnected -- Inform the parent the connection is up.
| WSJustClosed -- Inform the parent the connection is down.
2023-07-05 04:49:32 +02:00
| Log LogMessage
2023-07-03 18:03:47 +02:00
type Slot = H.Slot Query Output
2023-07-03 18:03:47 +02:00
data Query a
= ToSend ArrayBuffer a
2023-07-03 18:03:47 +02:00
data Action
= Initialize
| WebSocketParseError String
| ConnectWebSocket
| SendMessage ArrayBuffer
| Finalize
| HandleWebSocket (WebSocketEvent WebSocketMessageType)
type WSInfo
= { url :: String
, connection :: Maybe WS.WebSocket
, reconnect :: Boolean
}
2023-07-04 02:58:17 +02:00
type State = { wsInfo :: WSInfo }
2023-07-03 18:03:47 +02:00
component :: forall m. MonadAff m => H.Component Query Input Output m
component =
H.mkComponent
{ initialState
, render
, eval: H.mkEval $ H.defaultEval
{ initialize = Just Initialize
, handleAction = handleAction
, handleQuery = handleQuery
2023-07-03 18:03:47 +02:00
, finalize = Just Finalize
}
}
initialState :: Input -> State
initialState url =
2023-07-04 02:58:17 +02:00
{ wsInfo: { url: url
2023-07-03 18:03:47 +02:00
, connection: Nothing
, reconnect: false
}
}
render :: forall m. State -> H.ComponentHTML Action () m
2023-07-04 02:58:17 +02:00
render { wsInfo }
2023-07-03 18:03:47 +02:00
= HH.div_
2023-07-04 02:58:17 +02:00
[ renderReconnectButton (isNothing wsInfo.connection && wsInfo.reconnect)
2023-07-03 18:03:47 +02:00
]
where
renderFootnote :: String -> H.ComponentHTML Action () m
renderFootnote txt =
HH.div [ HP.style "margin-bottom: 0.125rem; color: grey;" ] [ HH.small_ [ HH.text txt ] ]
renderReconnectButton :: Boolean -> H.ComponentHTML Action () m
renderReconnectButton cond =
if cond
then
HH.p_
[ HH.button
[ HP.type_ HP.ButtonButton
, HE.onClick \_ -> ConnectWebSocket
]
[ HH.text "Reconnect?" ]
]
else
HH.p_
2023-07-07 20:29:49 +02:00
[ renderFootnote $
"NOTE: A 'Reconnect?' button will appear if the connection drops (for URL: '"
<>
wsInfo.url
<>
"')"
2023-07-03 18:03:47 +02:00
]
handleAction :: forall m. MonadAff m => Action -> H.HalogenM State Action () Output m Unit
handleAction action = do
{ wsInfo } <- H.get
case action of
Initialize ->
handleAction ConnectWebSocket
Finalize -> do
2023-07-05 04:49:32 +02:00
-- H.raise $ Log $ SystemLog $ "Closing websocket for '" <> wsInfo.url <> "'"
2023-07-03 18:03:47 +02:00
case wsInfo.connection of
2023-07-05 04:49:32 +02:00
Nothing -> H.raise $ Log $ SystemLog "No socket? How is that even possible?"
2023-07-03 18:03:47 +02:00
Just socket -> H.liftEffect $ WS.close socket
WebSocketParseError error ->
2023-07-05 04:49:32 +02:00
H.raise $ Log $ SystemLog $ renderError (UnknownError error)
2023-07-03 18:03:47 +02:00
ConnectWebSocket -> do
2023-07-05 04:49:32 +02:00
-- H.raise $ Log $ SystemLog $ "Connecting to \"" <> wsInfo.url <> "\"..."
2023-07-03 18:03:47 +02:00
webSocket <- H.liftEffect $ WS.create wsInfo.url []
H.liftEffect $ WS.setBinaryType webSocket ArrayBuffer
H.modify_ _ { wsInfo { connection = Just webSocket } }
void $ H.subscribe (HandleWebSocket <$> webSocketEmitter webSocket)
SendMessage array_buffer_to_send -> do
case wsInfo.connection of
2023-07-05 04:49:32 +02:00
Nothing -> H.raise $ Log $ SimpleLog $ "[🤖] Can't send a message, websocket is down!"
2023-07-03 18:03:47 +02:00
Just webSocket -> H.liftEffect $ do
sendArrayBuffer webSocket array_buffer_to_send
HandleWebSocket wsEvent -> do
case wsEvent of
WebSocketMessage received_message -> do
2023-07-05 04:49:32 +02:00
-- H.raise $ Log $ SimpleLog $ "[😈] Received a message"
2023-07-03 18:03:47 +02:00
H.raise $ MessageReceived $ Tuple wsInfo.url received_message.message
WebSocketOpen -> do
2023-07-05 04:49:32 +02:00
-- H.raise $ Log $ SystemLog ("Successfully connected to \"" <> wsInfo.url <> "\"!🎉")
2023-07-03 18:03:47 +02:00
H.raise $ WSJustConnected
WebSocketClose { code, reason, wasClean } -> do
2023-07-05 04:49:32 +02:00
H.raise $ Log $ SystemLog $ renderCloseMessage code wasClean reason
2023-07-03 18:03:47 +02:00
maybeCurrentConnection <- H.gets _.wsInfo.connection
when (isJust maybeCurrentConnection) do
H.modify_ _ { wsInfo { connection = Nothing, reconnect = true } }
H.raise $ WSJustClosed
WebSocketError errorType ->
2023-07-05 04:49:32 +02:00
H.raise $ Log $ SystemLog $ renderError errorType
2023-07-03 18:03:47 +02:00
-- TODO: MAYBE inform the parent the connection is closed (if it's the case).
where
renderCloseMessage
:: Int
-> Boolean
-> String
-> String
renderCloseMessage code wasClean = case _ of
"" -> baseCloseMessage
reason -> baseCloseMessage <> "Reason: " <> reason
where
baseCloseMessage :: String
baseCloseMessage =
String.joinWith " "
[ "Connection to WebSocket closed"
2023-07-05 03:18:01 +02:00
, "[ CODE:" , show code , "|" , if wasClean then "CLEAN" else "DIRTY" , "]"
2023-07-03 18:03:47 +02:00
]
handleQuery :: forall a m. MonadAff m => Query a -> H.HalogenM State Action () Output m (Maybe a)
handleQuery = case _ of
ToSend message a -> do
{ wsInfo } <- H.get
case wsInfo.connection of
Nothing -> do
2023-07-05 04:49:32 +02:00
H.raise $ Log $ UnableToSend "Not connected to server."
2023-07-03 18:03:47 +02:00
pure Nothing
Just webSocket -> do
H.liftEffect (WS.readyState webSocket) >>= case _ of
Connecting -> do
2023-07-05 04:49:32 +02:00
H.raise $ Log $ UnableToSend "Still connecting to server."
2023-07-03 18:03:47 +02:00
pure Nothing
Closing -> do
2023-07-05 04:49:32 +02:00
H.raise $ Log $ UnableToSend "Connection to server is closing."
2023-07-03 18:03:47 +02:00
pure Nothing
Closed -> do
2023-07-05 04:49:32 +02:00
H.raise $ Log $ UnableToSend "Connection to server has been closed."
2023-07-03 18:03:47 +02:00
maybeCurrentConnection <- H.gets _.wsInfo.connection
when (isJust maybeCurrentConnection) do
H.modify_ _ { wsInfo { connection = Nothing, reconnect = true } }
pure Nothing
Open -> do
H.liftEffect $ do
sendArrayBuffer webSocket message
pure (Just a)
2023-07-05 04:49:32 +02:00
--------------------------------------------------------------------------------
-- WebSocket mess.
--------------------------------------------------------------------------------
data WebSocketEvent :: Type -> Type
data WebSocketEvent msg
= WebSocketMessage { message :: msg, origin :: String, lastEventId :: String }
| WebSocketOpen
| WebSocketClose { code :: Int, reason :: String, wasClean :: Boolean }
| WebSocketError ErrorType
webSocketEmitter :: WS.WebSocket -> HS.Emitter (WebSocketEvent WebSocketMessageType)
webSocketEmitter socket = do
HS.makeEmitter \push -> do
openId <- HS.subscribe openEmitter push
errorId <- HS.subscribe errorEmitter push
closeId <- HS.subscribe closeEmitter push
messageId <- HS.subscribe messageEmitter push
pure do
HS.unsubscribe openId
HS.unsubscribe errorId
HS.unsubscribe closeId
HS.unsubscribe messageId
where
target = WS.toEventTarget socket
openEmitter :: HS.Emitter (WebSocketEvent WebSocketMessageType)
openEmitter =
HQE.eventListener WSET.onOpen target \_ ->
Just WebSocketOpen
errorEmitter :: HS.Emitter (WebSocketEvent WebSocketMessageType)
errorEmitter =
HQE.eventListener WSET.onError target \_ ->
Just (WebSocketError UnknownWebSocketError)
closeEmitter :: HS.Emitter (WebSocketEvent WebSocketMessageType)
closeEmitter =
HQE.eventListener WSET.onClose target \event ->
WSCE.fromEvent event >>= \closeEvent ->
Just $ WebSocketClose { code: WSCE.code closeEvent
, reason: WSCE.reason closeEvent
, wasClean: WSCE.wasClean closeEvent
}
messageEmitter :: HS.Emitter (WebSocketEvent WebSocketMessageType)
messageEmitter = HQE.eventListener WSET.onMessage target (WSME.fromEvent >=> decodeMessageEvent)
decodeMessageEvent :: WSME.MessageEvent -> Maybe (WebSocketEvent WebSocketMessageType)
decodeMessageEvent = \msgEvent -> do
let
foreign' :: F.Foreign
foreign' = WSME.data_ msgEvent
case foreignToArrayBuffer foreign' of
Left errs -> pure $ WebSocketError $ UnknownError errs
Right arrayBuffer -> pure $ WebSocketMessage { message: arrayBuffer
, origin: WSME.origin msgEvent
, lastEventId: WSME.lastEventId msgEvent }
---------------------------
-- Errors
---------------------------
data ErrorType
= UnknownError String
| UnknownWebSocketError
renderError :: ErrorType -> String
renderError = case _ of
UnknownError str ->
"Unknown error: " <> str
UnknownWebSocketError ->
"Unknown 'error' event has been fired by WebSocket event listener"
--------------------------------------------------------------------------------
-- WebSocket message type
--------------------------------------------------------------------------------
type WebSocketMessageType = ArrayBuffer
sendArrayBuffer :: WS.WebSocket -> ArrayBuffer -> Effect Unit
sendArrayBuffer = WS.sendArrayBuffer
foreignToArrayBuffer :: F.Foreign -> Either String ArrayBuffer
foreignToArrayBuffer
= lmap renderForeignErrors
<<< runExcept
<<< F.unsafeReadTagged "ArrayBuffer"
where
renderForeignErrors :: F.MultipleErrors -> String
renderForeignErrors =
String.joinWith "; " <<< A.fromFoldable <<< map F.renderForeignError