Copyright | (c) Justin Le 2015 |
---|---|
License | MIT |
Maintainer | justin@jle.im |
Stability | unstable |
Portability | ghcjs |
Safe Haskell | None |
Language | Haskell2010 |
Contains functions and operations for working with Javascript Websocket
connections, which are encapsulated in the Connection
object.
It includes operations for opening, closing, inspecting connections and
operations for sending and receiving text and serializable data
(instances of Binary
) through them.
Most of the necessary functionality is in hopefully in JavaScript.WebSockets; more of the low-level API is exposed in JavaScript.WebSockets.Internal if you need it for library construction.
- data Connection
- class WSSendable s
- class WSReceivable s
- data SocketMsg
- data ConnClosing
- = ManualClose
- | JSClose (Maybe Bool) (Maybe Int) (Maybe Text)
- | OpenInterruptedClose
- | UnexpectedClose Text
- data ConnectionException = ConnectionClosed {}
- withUrl :: Text -> (Connection -> IO a) -> IO a
- withUrlLeftovers :: Text -> (Connection -> IO a) -> IO (a, [SocketMsg])
- openConnection :: Text -> IO Connection
- closeConnection :: Connection -> IO ()
- closeConnectionLeftovers :: Connection -> IO [SocketMsg]
- clearConnectionQueue :: Connection -> IO ()
- dumpConnectionQueue :: Connection -> IO [SocketMsg]
- connectionClosed :: Connection -> IO Bool
- connectionCloseReason :: Connection -> IO (Maybe ConnClosing)
- connectionOrigin :: Connection -> Text
- sendData :: Binary a => Connection -> a -> IO Bool
- sendData_ :: Binary a => Connection -> a -> IO ()
- sendText :: Connection -> Text -> IO Bool
- sendText_ :: Connection -> Text -> IO ()
- sendMessage :: Connection -> SocketMsg -> IO Bool
- sendMessage_ :: Connection -> SocketMsg -> IO ()
- send :: WSSendable a => Connection -> a -> IO Bool
- send_ :: WSSendable a => Connection -> a -> IO ()
- receive :: WSReceivable a => Connection -> IO a
- receiveMaybe :: WSReceivable a => Connection -> IO (Maybe a)
- receiveText :: Connection -> IO Text
- receiveTextMaybe :: Connection -> IO (Maybe Text)
- receiveData :: Binary a => Connection -> IO a
- receiveDataMaybe :: Binary a => Connection -> IO (Maybe a)
- receiveMessage :: Connection -> IO SocketMsg
- receiveMessageMaybe :: Connection -> IO (Maybe SocketMsg)
- receiveEither :: WSReceivable a => Connection -> IO (Either SocketMsg a)
- receiveEitherMaybe :: WSReceivable a => Connection -> IO (Maybe (Either SocketMsg a))
Usage
import Data.Text (unpack) -- A simple echo client, echoing all incoming text data main :: IO () main = withUrl "ws://my-server.com" $ \conn -> forever $ do t <- receiveText conn putStrLn (unpack t) sendText conn t
The above code will attempt to interpret all incoming data as UTF8-encoded Text, and throw away data that does not.
conn
is a Connection
, which encapsulates a websocket channel.
You can also do the same thing to interpret all incoming data as any
instance of Binary
--- say, Int
s:
-- A simple client waiting for connections and outputting the running sum main :: IO () main = withUrl "ws://my-server.com" (runningSum 0) runningSum :: Int -> Connection -> IO () runningSum n conn = do i <- receiveData conn print (n + i) runningSum (n + i) conn
receiveData
will block until the Connection
receives data that is
decodable as whatever type you expect, and will throw away all
nondecodable data (including Text
data).
The receive
function is provided as a over-indulgent layer of
abstraction where you can receive both Text
and instances of Binary
with the same function using typeclass magic --- for the examples above,
you could use receive
in place of both receiveText
and
receiveData
.
send
works the same way for sendText
and sendData
.
If you want to, you can access the incoming data directly using the
SocketMsg
sum type, which exposes either a Text
or a lazy
ByteString
:
import Data.Text (unpack, append) import qualified Data.ByteString.Base64.Lazy as B64 main :: IO () main = withUrl "ws://my-server.com" $ \conn -> forever $ do msg <- receiveMessage putStrLn $ case msg of SocketMsgText t -> unpack $ append "Received text: " t SocketMsgData d -> "Received data: " ++ show (B64.encode d)
You can talk to multiple connections by nesting withUrl
:
-- Act as a relay between two servers main :: IO () main = withUrl "ws://server-1.com" $ \conn1 -> withUrl "ws://server-2.com" $ \conn2 -> forever $ do msg <- receiveMessage conn1 sendMessage conn2 msg
And also alternatively, you can manually open and close connections:
-- Act as a relay between two servers main :: IO () main = do conn1 <- openConnection "ws://server-1.com" conn2 <- openConnection "ws://server-2.com" forever $ do msg <- receiveMessage conn1 sendMessage conn2 msg closeConnection conn2 closeConnection conn1
receiveMessage
and its varieties will all throw an exception if the
connection closes while they're waiting or if you attempt to receive on
a closed connection. You can handle these with mechanisms from
Control.Exception, or you can use their "maybe"-family counterparts,
receiveMessageMaybe
, etc., who will return results in Just
on
a success, or return a Nothing
if the connection is closed or if
receiving on a closed connection.
You can use also
to
check if the given connectionClosed
:: Connection
-> IO
Bool
Connection
object is closed (or
connectionCloseReason
to see *why*).
When closing connections, there might be some messages that were
received by the socket but never processed on the Haskell side with
a receive
method. These will normally be deleted; however, you can
use closeConnectionLeftovers
or withUrlLeftovers
to grab a list of
the raw SocketMsg
s remaining after closing.
data Connection
Encapsulates a (reference to a) Javascript Websocket connection. Can
be created/accessed with either openConnection
or (preferably)
withUrl
.
Care must be taken to close the connection once you are done if using
openConnection
, or unprocessed messages and callbacks will continue to
queue up.
class WSSendable s
A typeclass offering a gratuitous abstraction over what can be sent
through a Connection
. Allows you to wrap things in a SocketMsg
automatically. The only instances that should really ever exist are
Text
and instances of Binary
.
Binary a => WSSendable a | |
WSSendable Text |
class WSReceivable s
A typeclass offering a gratuitous abstraction over what can be
received through a Connection
. Allows you to unwrap things in
a SocketMsg
automatically. The only instances that should really ever
exist are Text
and instances of Binary
.
Binary a => WSReceivable a | |
WSReceivable Text |
data SocketMsg
Sum type over the data that can be sent or received through a JavaScript websocket.
What an incoming message is classified as depends on the Javascript Websockets API http://www.w3.org/TR/websockets/, which provides a "typed" input channel of either text or binary blob.
There are several convenience functions to help you never have to deal
with this explicitly; its main purpose is if you want to explicitly
branch on a receiveMessage
depending on what kind of message you
receive and do separate things. receiveText
and receiveData
will
both allow you to "filter" incoming messages by their type.
data ConnClosing
Data type containing information on Connection
closes.
ManualClose
: Closed by the HaskellWebSockets
interface, usingcloseConnection
or variants.JSClose
: Closed on the Javascript end, either by a connection error or server request, or what have you. Contains information from the Javascript Websockets API http://www.w3.org/TR/websockets/#event-definitions.The first field is whether or not it was a clean close; the second field is the closing reason code; the third field is a
Text
with the reason given by the Websockets API.OpenInterupptedClose
: There was an unexpected error encountered when attempting to open the connection.UnexpectedClose
: Otherwise uncategorized closed status, with aText
field offering a reason.
data ConnectionException
An exception that may be thrown when using the various Connection
operations. Right now, only includes ConnectionClosed
, which is
thrown when using an "unsafe" receive
on a closed Connection
, or if
a Connection
closes while an unsafe receive
is waiting.
Opening, closing, and working with connections
:: Text | Websocket address to connect to |
-> (Connection -> IO a) | Process to run on connection |
-> IO a |
Performs the given Connection -> IO a
process attached to the given
server url. Handles opening and closing the Connection
for you (and
clearing the message queue afterwards), and cleans up on errors.
If any messages were received by the socket but never processed/received
on the Haskell end, this will delete and drop them. Use
withUrlLeftovers
to get a hold of them.
:: Text | Websocket address to connect to |
-> (Connection -> IO a) | Process to run on connection |
-> IO (a, [SocketMsg]) | Result of process, with leftovers |
openConnection :: Text -> IO Connection
Opens a websocket connection to the given url, and returns the
Connection
after connection is completed and opened. Care should be
taken to ensure that the Connection
is later closed with
closeConnection
.
Consider using withUrl
, which handles closing with bracketing and
error handling so you don't have to worry about closing the connection
yourself.
Blocks until the connection has been established and opened.
If an async exception happens while this is waiting, the socket will be closed as the exception bubbles up.
closeConnection :: Connection -> IO ()
Manually closes the given Connection
. Will un-block all threads
currently waiting on the Connection
for messages (releasing their
callbacks) and disable sending and receiving in the future.
All leftover messages that were never processed on the Haskell end will
be deleted; use dumpConnectionQueue
to manually fetch them before
closing, or closeConnectionLeftovers
to recover them while closing.
closeConnectionLeftovers :: Connection -> IO [SocketMsg]
Manually closes the given Connection
. It un-blocks all threads
currently waiting on the connection and disables all sending and
receiving in the future.
The result is a list of all messages received by the connection but not
yet retrieved by receive
, etc. on the Haskell end.
To close and ignore leftovers, use closeConnection
.
clearConnectionQueue :: Connection -> IO ()
Clears the message queue (messages waiting to be receive
d) on the
given Connection
. Is essentially a no-op on closed connections.
dumpConnectionQueue :: Connection -> IO [SocketMsg]
Returns all incoming messages received by the socket and queued for
retrieval using receive
functions. Empties the queue.
connectionClosed :: Connection -> IO Bool
Check if the given Connection
is closed. Returns a Bool
. To
check *why* it was closed, see connectionCloseReason
.
connectionCloseReason :: Connection -> IO (Maybe ConnClosing)
Returns Nothing
if the given Connection
is still open, or Just
closing
containing a ConnClosing
with information on why the
connection was closed.
For just a Bool
saying whether or not the connection is closed, try
connectionClosed
.
connectionOrigin :: Connection -> Text
Returns the origin url of the given Connection
.
Sending data
sendData :: Binary a => Connection -> a -> IO Bool
sendData_ :: Binary a => Connection -> a -> IO ()
sendText :: Connection -> Text -> IO Bool
sendText_ :: Connection -> Text -> IO ()
sendMessage :: Connection -> SocketMsg -> IO Bool
Sends the given SocketMsg
through the given Connection
.
A SocketMsg
is a sum type of either 'SocketMsgText t', containing
(strict) Text
, or 'SocketMsgData d', containing a (lazy) ByteString
.
Returns True
if the connection is open, and False
if it is closed.
In the future will return more feedback about whether or not the send
was completed succesfully.
sendMessage_ :: Connection -> SocketMsg -> IO ()
Sends the given SocketMsg
through the given Connection
.
A SocketMsg
is a sum type of either 'SocketMsgText t', containing
(strict) Text
, or 'SocketMsgData d', containing a (lazy) ByteString
.
Fails silently if the connection is closed or otherwise was not
succesful. Use sendMessage
to get feedback on the result of the send.
send :: WSSendable a => Connection -> a -> IO Bool
Send the given item through the given Connection
.
You can send
either (strict) Text
or any instance of Binary
,
due to over-indulgent typeclass magic; this is basically a function that
works everywhere you would use sendText
or sendData
.
Returns True
if the connection is open, and False
if it is closed.
In the future will return more feedback about whether or not the send
was completed succesfully.
send_ :: WSSendable a => Connection -> a -> IO ()
Send the given item through the given Connection
.
You can send_
either (strict) Text
or any instance of Binary
,
due to over-indulgent typeclass magic; this is basically a function that
works everywhere you would use sendText_
or sendData_
.
Fails silently if the connection is closed or otherwise was not
succesful. Use send
to get feedback on the result of the send.
Receiving data
receive :: WSReceivable a => Connection -> IO a
Block and wait until either something decodable as the desired type is
received (returning it), or the Connection
closes (throwing
a ConnectionException
). Throws the exception immediately if the
Connection
is already closed and there are no queued messages left.
This is polymorphic on its return type, so remember to let the type
inference system know what you want at some point or just give an
explicit type signature --- receiveData conn :: IO (Maybe Int)
, for
example.
All non-decodable or non-matching data that comes along is discarded.
You can receive
either (strict) Text
or any instance of Binary
,
due to over-indulgent typeclass magic; this is basically a function that
works everywhere you would use receiveText
or receiveData
.
To handle closed sockets with Maybe
, use receiveMaybe
.
receiveMaybe :: WSReceivable a => Connection -> IO (Maybe a)
Block and wait until either something decodable as the desired type is
received (returning Just x
), or the Connection
closes (returning
Nothing
). Returns Nothing
immediately if the Connection
is
already closed and there are no queued messages left.
This is polymorphic on its return type, so remember to let the type
inference system know what you want at some point or just give an
explicit type signature --- receiveData conn :: IO (Maybe Int)
, for
example.
All non-decodable or non-matching data that comes along is discarded.
You can receive
either (strict) Text
or any instance of Binary
,
due to over-indulgent typeclass magic; this is basically a function that
works everywhere you would use receiveText
or receiveData
.
receiveText :: Connection -> IO Text
Block and wait until the Connection
receives a "typed" Text
. This
is determined by Javascript's own "typed" Websockets API
http://www.w3.org/TR/websockets/, which receives data
typed either as text or as a binary blob. Returns the first encountered
text. Throws a ConnectionException
if the Connection
closes first,
and throws one immediately if the connection is already closed and there
are no queued messages left.
All "binary blobs" encountered are discarded.
To handle closed sockets with Maybe
, use receiveTextMaybe
.
receiveTextMaybe :: Connection -> IO (Maybe Text)
Block and wait until the Connection
receives a "typed" Text
. This
is determined by Javascript's own "typed" Websockets API
http://www.w3.org/TR/websockets/, which receives data typed either as
text or as a binary blob. Returns Just t
on the first encountered
text. Returns Nothing
if the Connection
closes while it is waiting,
or immediately if the connection is already closed and there are no
queued messages left.
All "binary blobs" encountered are discarded.
receiveData :: Binary a => Connection -> IO a
Block and wait until the Connection
receives a "binary blob"
decodable as the desired instance of Binary
. Returns the first
succesfully decoded data, and throws a ConnectionException
if the
Connection
closes first. Throws the exception immediately if the
Connection
is already closed and there are no queued messages left.
This is polymorphic on its return type, so remember to let the type
inference system know what you want at some point or just give an
explicit type signature --- receiveData conn :: IO (Maybe Int)
, for
example.
All incoming messages received that cannot be decoded as the data type (or are text) will be discarded.
To handle closed sockets with Maybe
, use receiveDataMaybe
.
receiveDataMaybe :: Binary a => Connection -> IO (Maybe a)
Block and wait until the Connection
receives a "binary blob"
decodable as the desired instance of Binary
. Returns Just x
as soon
as it is able to decode a blob, and Nothing
if the Connection
closes
while it is waiting. Returns Nothing
immediately if the Connection
is already closed and there are no queued messages left.
All incoming messages received that cannot be decoded as the data type (or are text) will be discarded.
This is polymorphic on its return type, so remember to let the type
inference system know what you want at some point or just give an
explicit type signature --- receiveData conn :: IO (Maybe Int)
, for
example.
receiveMessage :: Connection -> IO SocketMsg
Block and wait until the Connection
receives any message, and
returns the message wrapped in a SocketMsg
. A SocketMsg
is a sum
type of either 'SocketMsgText t', containing (strict) Text
, or
'SocketMsgData d', containing a (lazy) ByteString
.
Will return the message as soon as any is received, or throw
a ConnectionException
if the connection is closed while waiting.
Throws an exception immediately if the connection is already closed.
To handle closed sockets with Maybe
, use receiveMessageMaybe
.
receiveMessageMaybe :: Connection -> IO (Maybe SocketMsg)
Block and wait until the Connection
receives any message, and
returns the message wrapped in a SocketMsg
. A SocketMsg
is a sum
type of either 'SocketMsgText t', containing (strict) Text
, or
'SocketMsgData d', containing a (lazy) ByteString
.
Will return 'Just msg' as soon as any message is received, or Nothing
if the Connection
closes first. Returns Nothing
immediately if the
Connection
is already closed.
receiveEither :: WSReceivable a => Connection -> IO (Either SocketMsg a)
Block and wait until the Connection
receives any message, and
attempts to decode it depending on the desired type. If Text
is
requested, assumes Utf8-encoded text or just a plain Javascript string.
If an instance of Binary
is requested, attempts to decode it into that
instance. Successful parses return 'Right x', and failed parses return
'Left SocketMsg' (A sum type between SocketMsgText
containing (strict)
Text
and SocketMsgData
containing a (lazy) ByteString
). Nothing
is ever discarded.
Will return the message as soon as any is received, or throw
a ConnectionException
if the connection is closed while waiting.
Throws an exception immediately if the connection is already closed and
there are no queued messages left.
To handle closed sockets with Maybe
, use receiveEitherMaybe
.
receiveEitherMaybe :: WSReceivable a => Connection -> IO (Maybe (Either SocketMsg a))
Block and wait until the Connection
receives any message, and
attempts to decode it depending on the desired type. If Text
is
requested, assumes Utf8-encoded text or just a plain Javascript string.
If an instance of Binary
is requested, attempts to decode it into that
instance. Successful parses return 'Right x', and failed parses return
'Left SocketMsg' (A sum type between SocketMsgText
containing (strict)
Text
and SocketMsgData
containing a (lazy) ByteString
). Nothing
is ever discarded.
Returns Just result
on the first message received, or Nothing
if the
Connection
closes while waiting. Returns Nothing
if the connection
is already closed and there are no queued messages left.