Giter Club home page Giter Club logo

reflex-websocket-interface's Introduction

Consider using https://github.com/reflex-frp/reflex-gadt-api which implements websockets + XHR

Websocket Interface for Reflex applications

This repository consists of three packages to be used together, along with reflex as Dom builder, to do websocket communication.

Highlights

Uses type operators and Generic to create the request sum type, and avoid writing most of the server side boiler plate code.

type Request = Request1 :<|> Request2 :<|> Request3

-- Well almost... see below for exact usage
handler = handleRequest1 :<&> handleRequest2 :<&> handleRequest3
  1. Request <-> Response

    Type-checker make sure your client and server APIs are in sync.

  2. Completeness of server handler

    The Type-checker also make sure that all the requests are handled by the server side code.

  3. Client side monadic interface with routing of response event.

    In the client application the requests come from different parts of the DOM and their responses need to be routed back to the same place. This is all taken care by the library code and the user interface is as simple as calling an API

    getResponse :: Event t request -> m (Event t response)

Usage

See the code in example folder for more details.

  1. Shared code

    Create a shared code which has the websocket message type using the package reflex-websocket-interface-shared

    Define the types of all the requests and their corresponding responses. Also derive Generic instance of all the individual request and response types. This can be used to automatically derive the ToJSON and FromJSON instances also.

    The collection of all websocket requests which can happen over a single connection are grouped together using the type operator (:<|>). In the rest of document this type is called referred as the request-type.

    type Request = Request1 :<|> Request2 :<|> Request3
    
    data Request1 = Request1 Text
      deriving (Generic, Show)
    data Request2 = Request2 (Text, Text)
      deriving (Generic, Show)
    data Request3 = Request3 [Text]
      deriving (Generic, Show)
    
    data Response1 = Response1 Int
      deriving (Generic, Show)
    data Response2 = Response2 Text
      deriving (Generic, Show)
    data Response3 = Response3 (Text, Int)
      deriving (Generic, Show)

    Next specify the WebSocketMessage instances for each of individual requests contained in the request-type

    instance WebSocketMessage Request Request1 where
      type ResponseT Request Request1 = Response1
    
    instance WebSocketMessage Request Request2 where
      type ResponseT Request Request2 = Response2
    
    instance WebSocketMessage Request Request3 where
      type ResponseT Request Request3 = Response3
  2. Frontend

    In the reflex application use the reflex-websocket-interface package.

    Use the getWebSocketResponse API along with the other DomBuilder code to create the widget in the WithWebSocketT monad.

      -- req1 :: Event t Request1
      -- respEv1 :: Event t Response1
      respEv1 <- getWebSocketResponse req1

    and specify this widget in withWSConnection API along with the websocket url to run the widget using the websocket connection.

      (retVal,wsConn) <- withWSConnection
         url wsCloseEvent doRecconectBool widgetCode
  3. Server

    Use the reflex-websocket-interface-server package

    Specify all the handlers like this (m can be any monad, Use Identity monad if the handler code is pure)

    handleRequest1 :: (Monad m) => Request1 -> m Response1
    handleRequest2 :: (Monad m) => Request2 -> m Response2

    Create a main handler using all the individual handler using the type operator (:<&>) and the makeHandler API. You need to specify the request-type explicitly in the HandlerWrapper and makeHandler like this

    handler :: HandlerWrapper IO Request
    handler = HandlerWrapper $
      h handleRequest1
      :<&> h handleRequest2
      where
        h :: (WebSocketMessage Request a, Monad m)
          => (a -> m (ResponseT Request a))
          -> Handler m Request a
        h = makeHandler

    Use this handler in the handleRequest API, also specify the bytestring received from the websocket connection. This API will run the appropriate handler based on the request type and encode the response back in bytestring.

    -- resp :: Bytestring
    -- showF :: (Show a, Show b) => a -> b -> m ()
    resp <- handleRequest handler showF bsRecieved

Here I have used the aeson package for serialisation. To use some other serialisation package the library code will need slight modifications, but should work.

reflex-websocket-interface's People

Contributors

3noch avatar dfordivam avatar marcotoniut avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

reflex-websocket-interface's Issues

How do you do push notification?

Is it possible to use your library and have the server push content down to the client, unrequested? Or perhaps the client could make a request and the server could push multiple responses, over time?

Better Show, ToJSON, FromJSON instances for requests

For deeply nested requests there is a lot of overhead with ToJSON/FromJSON instances

{"tag":"Recurse","contents":{"tag":"Recurse","contents":{"tag":"Recurse","contents":{"tag":"Recurse","contents":{"tag":"Recurse","contents":{"tag":"Recurse","contents":{"tag":"Recurse","contents":{"tag":"Recurse","contents":{"tag":"Recurse","contents":{"tag":"Terminal","contents":["SomeType",[],null]}}}}}}}}}}]

Also a better Show instance will be useful in debugging

Make a type alias for the requester constraint

Reflex.Dom.WebSocket.Monad should probably export IsWebSocketRequest and IsWebSocketResponse. Also handy would be a constraint to easily allow users to be polymorphic on it:

type HasWebSocket t msg m = Requester t (IsWebSocketRequest msg) (IsWebSocketResponse msg) m

Request tracking

Hi,

first of, thank you for publishing this! It's been a huge help.

Second, I have a question, a bit of a nitpick really, regarding how the library handles state associated with open requests client side.

If I read things correctly, basically we're keeping a Map Int (RequesterDataKey a) that allows us to track open requests and map Int identifiers we can serialize and send over the network to opaque tags produced by requester. So that way, when a response comes back we know how to find the original RequesterDataKey that we need to give back to Requester.

What happens to requests that produce no response from the server for whatever reason?

Currently I guess the request map is not "GCed" in any way. Nor is there a way to force cleanup if one detects such a condition by other means. It wouldn't be a problem under normal conditions but do you think it would be good to have a configurable timeout and periodically clean the request map or another mechanism maybe?

Or is there a reason this is non-issue that I'm not seeing right now? It seems in a particularly nasty case these request could stack over a long period of time and affect performance here at the very least. Of course then you have other problems.

Surely there must be a way to this without potentially leaking memory.

Fails to build with latest reflex-platform/reflex-dom

If you use the latest reflex-platform, you get an error when building this:

Reflex/Dom/WebSocket/Monad.hs:76:43: error:
    • Couldn't match type ‘DMap
                             (Tag RealWorld) (IsWebSocketResponse ws0)’
                     with ‘RequesterData (IsWebSocketResponse ws)’
      Expected type: Event t (RequesterData (IsWebSocketResponse ws))
        Actual type: Event
                       t (DMap (Tag RealWorld) (IsWebSocketResponse ws0))
    • In the second argument of ‘runRequesterT’, namely ‘respEvMap’
      In a stmt of a 'do' block:
        (val, reqEvMap) <- runRequesterT wdgt respEvMap
      In a stmt of a 'do' block:
        rec { let respEvMap
                    = getResponseFromBS tagsDyn (_webSocket_recv ws);
              (val, reqEvMap) <- runRequesterT wdgt respEvMap;
              let bsAndMapEv = getRequestBS reqEvMap
                  sendEv = fst <$> bsAndMapEv
                  conf = WebSocketConfig sendEv closeEv reconnect;
              tagsDyn <- foldDyn (<>) Map.empty (snd <$> bsAndMapEv);
              ws <- webSocket url conf }
    • Relevant bindings include
        respEvMap :: Event
                       t (DMap (Tag RealWorld) (IsWebSocketResponse ws0))
          (bound at Reflex/Dom/WebSocket/Monad.hs:74:7)
        bsAndMapEv :: Event t ([ByteString], TagMap ws0)
          (bound at Reflex/Dom/WebSocket/Monad.hs:79:7)
        reqEvMap :: Event t (RequesterData (IsWebSocketRequest ws))
          (bound at Reflex/Dom/WebSocket/Monad.hs:76:11)
        tagsDyn :: Dynamic t (TagMap ws0)
          (bound at Reflex/Dom/WebSocket/Monad.hs:83:5)
        wdgt :: WithWebSocketT ws t m a
          (bound at Reflex/Dom/WebSocket/Monad.hs:71:40)
        withWSConnection :: Text
                            -> Event t (Word, Text)
                            -> Bool
                            -> WithWebSocketT ws t m a
                            -> m (a, Reflex.Dom.WebSocket t)
          (bound at Reflex/Dom/WebSocket/Monad.hs:71:1)

Reflex/Dom/WebSocket/Monad.hs:79:33: error:
    • Couldn't match type ‘RequesterData (IsWebSocketRequest ws)’
                     with ‘DMap (Tag RealWorld) (IsWebSocketRequest ws0)’
      Expected type: Event
                       t (DMap (Tag RealWorld) (IsWebSocketRequest ws0))
        Actual type: Event t (RequesterData (IsWebSocketRequest ws))
    • In the first argument of ‘getRequestBS’, namely ‘reqEvMap’
      In the expression: getRequestBS reqEvMap
      In an equation for ‘bsAndMapEv’: bsAndMapEv = getRequestBS reqEvMap
    • Relevant bindings include
        bsAndMapEv :: Event t ([ByteString], TagMap ws0)
          (bound at Reflex/Dom/WebSocket/Monad.hs:79:7)
        reqEvMap :: Event t (RequesterData (IsWebSocketRequest ws))
          (bound at Reflex/Dom/WebSocket/Monad.hs:76:11)
        respEvMap :: Event
                       t (DMap (Tag RealWorld) (IsWebSocketResponse ws0))
          (bound at Reflex/Dom/WebSocket/Monad.hs:74:7)
        tagsDyn :: Dynamic t (TagMap ws0)
          (bound at Reflex/Dom/WebSocket/Monad.hs:83:5)
        wdgt :: WithWebSocketT ws t m a
          (bound at Reflex/Dom/WebSocket/Monad.hs:71:40)
        withWSConnection :: Text
                            -> Event t (Word, Text)
                            -> Bool
                            -> WithWebSocketT ws t m a
                            -> m (a, Reflex.Dom.WebSocket t)
          (bound at Reflex/Dom/WebSocket/Monad.hs:71:1)

Async requests

It seems like websocket requests are synchronous (blocking). Is this true? If so, would it be possible to use async instead?

Using the same underlying message type *seems* to cause infinite loop

My messages looked like this:

type WsRequest = ReqGetUsers :<|> ReqGetUser :<|> ReqPutUser :<|> ReqAddUser
data ReqGetUsers = ReqGetUsers  -- response type was RespGetUsers [User]
data ReqGetUser = ReqGetUser Int -- response type was RespGetUser (Maye User)
data ReqPutUser = ReqPutUser User -- response type was RespPutUser (Either Text ())
data ReqAddUser = ReqAddUser User  -- response type was RespAddUser (Either Text User)

Everything worked fine until I added that fourth message. After that I saw behavior where requesting either ReqPutUser or ReqAddUser would cause the app to go into an infinite loop and eventually crash. The first two messages still worked fine.

After many hours of trying to find the loop I tried one crazy idea. I removed ReqAddUser and once again ReqPutUser started to work. So I changed ReqAddUser to this:

data ReqAddUser = ReqAddUser () User

And suddenly all 4 of them started to work!

Could it be that because two messages carried the same type that some type-level encoding treated them the same? Then when trying to look for it, it would somehow look forever?

Allow metadata to be captured with requests

servant-reflex has the notion of a "tag" (not to be confused with tagging Events, etc.) where an XHR request can be "tagged" with some meta information that will be given back with the response. The meta information has no role in the XHR interaction itself. It's merely a "snapshot" of some state at the time the request was made. This is helpful to track the value of things at the time of a request, even if those values may have changed since the request was made.

It would be useful to have such a feature here as well. For example, I would use it keep track of the form data that was posted via a websocket request. I don't actually want the websocket to send back the form data. I just need to know what that form data was when I made the request. As long as the request comes back with "Ok" then I know the form data in the "tag" is correct. But I can avoid having to send it back in the websocket.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.