-
Notifications
You must be signed in to change notification settings - Fork 201
/
Copy pathWebSocket.hs
111 lines (93 loc) · 4.42 KB
/
WebSocket.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
{-|
Module: IHP.WebSocket
Description: Building blocks for websocket applications
Copyright: (c) digitally induced GmbH, 2020
-}
{-# LANGUAGE AllowAmbiguousTypes #-}
module IHP.WebSocket
( WSApp (..)
, startWSApp
, setState
, getState
, receiveData
, receiveDataMessage
, sendTextData
, sendJSON
)
where
import IHP.Prelude
import qualified Network.WebSockets as Websocket
import Network.WebSockets.Connection.PingPong (withPingPong, defaultPingPongOptions)
import IHP.ApplicationContext
import IHP.Controller.RequestContext
import qualified Data.UUID as UUID
import qualified Data.Maybe as Maybe
import qualified Control.Exception.Safe as Exception
import IHP.Controller.Context
import qualified Data.Aeson as Aeson
import qualified IHP.Log as Log
import qualified Network.WebSockets.Connection as WebSocket
class WSApp state where
initialState :: state
run :: (?state :: IORef state, ?context :: ControllerContext, ?applicationContext :: ApplicationContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection) => IO ()
run = pure ()
onPing :: (?state :: IORef state, ?context :: ControllerContext, ?applicationContext :: ApplicationContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection) => IO ()
onPing = pure ()
onClose :: (?state :: IORef state, ?context :: ControllerContext, ?applicationContext :: ApplicationContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection) => IO ()
onClose = pure ()
-- | Provide WebSocket Connection Options
--
-- See All Config Options Here
-- https://hackage.haskell.org/package/websockets/docs/Network-WebSockets-Connection.html#t:ConnectionOptions
--
-- __Example:__
-- Enable default permessage-deflate compression
--
-- > connectionOptions =
-- > WebSocket.defaultConnectionOptions {
-- > WebSocket.connectionCompressionOptions =
-- > WebSocket.PermessageDeflateCompression WebSocket.defaultPermessageDeflate
-- > }
--
connectionOptions :: WebSocket.ConnectionOptions
connectionOptions = WebSocket.defaultConnectionOptions
startWSApp :: forall state. (WSApp state, ?applicationContext :: ApplicationContext, ?requestContext :: RequestContext, ?context :: ControllerContext, ?modelContext :: ModelContext) => state -> Websocket.Connection -> IO ()
startWSApp initialState connection = do
state <- newIORef initialState
let ?state = state
result <- Exception.try ((withPingPong defaultPingPongOptions connection (\connection -> let ?connection = connection in run @state)) `Exception.finally` (let ?connection = connection in onClose @state))
case result of
Left ([email protected]{}) ->
case Exception.fromException e of
(Just Websocket.ConnectionClosed) -> pure ()
(Just (Websocket.CloseRequest {})) -> pure ()
(Just other) -> error ("Unhandled Websocket exception: " <> show other)
Nothing -> Log.error (tshow e)
Right _ -> pure ()
setState :: (?state :: IORef state) => state -> IO ()
setState newState = writeIORef ?state newState
getState :: (?state :: IORef state) => IO state
getState = readIORef ?state
receiveData :: (?connection :: Websocket.Connection, Websocket.WebSocketsData a) => IO a
receiveData = Websocket.receiveData ?connection
receiveDataMessage :: (?connection :: Websocket.Connection) => IO Websocket.DataMessage
receiveDataMessage = Websocket.receiveDataMessage ?connection
sendTextData :: (?connection :: Websocket.Connection, Websocket.WebSocketsData text) => text -> IO ()
sendTextData text = Websocket.sendTextData ?connection text
-- | Json encode a payload and send it over the websocket wire
--
-- __Example:__
--
-- > message <- Aeson.decode <$> receiveData @LByteString
-- >
-- > case message of
-- > Just decodedMessage -> handleMessage decodedMessage
-- > Nothing -> sendJSON FailedToDecodeMessageError
--
sendJSON :: (?connection :: Websocket.Connection, Aeson.ToJSON value) => value -> IO ()
sendJSON payload = sendTextData (Aeson.encode payload)
instance Websocket.WebSocketsData UUID where
fromDataMessage (Websocket.Text byteString _) = UUID.fromLazyASCIIBytes byteString |> Maybe.fromJust
fromDataMessage (Websocket.Binary byteString) = UUID.fromLazyASCIIBytes byteString |> Maybe.fromJust
fromLazyByteString byteString = UUID.fromLazyASCIIBytes byteString |> Maybe.fromJust
toLazyByteString = UUID.toLazyASCIIBytes