5

It would be nice to be able to use the Network.WebSockets module from inside a snaplet, but I can't figure out how to actually do it.

Using the runWebSocketsSnap :: MonadSnap m => ServerApp -> m () function from Network.WebSockets.Snap it is easy to include a simple stateless websocket server in my app:

routes :: [(ByteString, Handler App App ())]
routes = [ ("/ws", runWebSocketsSnap wsApp) ]

wsApp :: PendingConnection -> IO () -- this is the ServerApp type
wsApp pending = do
    conn <- acceptRequest pending
    forever $ do
        msg <- receiveData conn
        sendTextData conn ("Echo " `mappend` msg :: Text)

But my goal is to maintain a state for the webscket server (for example, a list of the connected clients, as in http://jaspervdj.be/websockets/example.html). Alternatively, access to the acid-state store of the snaplet would be great.

My first idea was to liftIO the websocket actions into the Handler App App monad, and write an app like this:

wsApp :: PendingConnection -> Handler App App ()
wsApp pending = do
    conn <- liftIO $ acceptRequest pending
    forever $ do
        msg <- liftIO $ receiveData conn
        update (SetLastMsg msg)
        liftIO $ sendTextData conn ("Stored msg in datastore.")

But there is no version of runWebSocketsSnap that takes an app of the above form, and I can't figure out how to modify the existing one (source on hackage). It seems to me one would need an alternative to forkIO that takes an action in the Handler App App monad instead, but my understanding of Haskell and especially concurrency in Snap ends here...

dermoritz
  • 187
  • 8

1 Answers1

4

The runWebSocketsSnap function requires that its argument be of type PendingConnection -> IO (). This means that you can't access your App data structure inside of that function directly. What you need to do is pass the information to the function as an argument something like this.

routes = [ ("/ws", webSocketsDriver) ]

webSocketsDriver :: Handler App App ()
webSocketsDriver = do
    appState <- get
    runWebSocketsSnap (wsApp appState)

wsApp :: App -> PendingConnection -> IO ()
wsApp app pending = do
    ...
mightybyte
  • 7,282
  • 3
  • 23
  • 39
  • Initially, this didn't seem to stateful to me. But, I suppose you can throw from TVars, MVars, or IORefs in app for managing state, if you need to. – Boyd Stephen Smith Jr. Mar 25 '14 at 22:14
  • I agree with @boyd-stephen-smith-jr, and this solves my problem. In particular, I can do things like `acidState <- getAcidState`, pass it to wsApp and then use acid-state from there: `Data.Acid.update acidState (SetLastMsg msg)` – dermoritz Mar 26 '14 at 03:52
  • The Handler monad is inherently stateful because it is making App (or b and v) available to you without explicitly passing it in. Also, if you want to make it simpler, you can define the wsApp function inline (after the `get` line, do `let wsApp = ...`) and then you don't have to pass anything in. – mightybyte Mar 26 '14 at 17:10