8

How do I define a Server-Sent Event(SSE) end point for servant. The docs don't seem to cover this case.

If Servant is not designed for the realtime use case, which Haskell server framework supports SSE?

Subra
  • 105
  • 5
  • I would be interested if you could update question with working example of this problem, or can you share link to public repo? – akegalj Oct 24 '16 at 17:06

4 Answers4

5

servant uses WAI, and you can always dip down into normal WAI applications and all the libraries that exist for it with the Raw combinator. So you can use Network.Wai.EventSource from wai-extra to create an Application, which is the type of handlers for Raw endpoints. Something like:

type MyApi = "normalapi" :> NormalApi 
        :<|> "sse" :> Raw

myServer :: Server MyAPI
myServer = normalServer :<|> eventSourceAppChan myChan
user2141650
  • 2,827
  • 1
  • 15
  • 23
  • That worked! I had to work around some `ByteString` vs `Builder` issues but post that worked very well at 1000 connected streams. I will try and factor it out as its own thing and see if I publish it as a library – Subra Jun 17 '16 at 02:10
2

Thanks to the answer of user2141650 I managed to get a working example of a server-sent events that uses channels.

The gist of the solution is as follows. Assume that we have an echo server that just echoes messages:

newtype Message = Message { msgText :: Text }

Then we'll define three end-points, one for creating sessions, one for sending messages to a session, and the other for retrieving the messages of a session using server-sent events:

# Create a new session
curl -v -XPOST http://localhost:8081/session/new
# And subscribe to its events
curl -v http://localhost:8081/events/0
# And from another terminal
curl -v -XPOST http://localhost:8081/session/0/echo\
     -H "Content-Type: application/json" -d '{"msgText": "Hello"}'

Now let's see how to implement the end-point to write a message for a given session, into a channel:

sendH :: SessionId -> Message -> Handler NoContent
sendH sid msg = do
    -- lookupChannel :: Env -> SessionId -> IO (Maybe (Chan ServerEvent))
    mCh <- liftIO $ lookupChannel env sid
    case mCh of
        Nothing ->
            throwError err404
        Just ch -> do
            liftIO $ writeChan ch (asServerEvent msg)
            return NoContent

The function to convert a Message to a ServerEvent is shown below:

import           Data.Text.Encoding          as TE
import qualified Data.Text.Lazy              as T

asServerEvent :: Message -> ServerEvent
asServerEvent msg = ServerEvent
    { eventName = Just eName
    , eventId = Nothing
    , eventData = [msg']
    }
    where
      eName :: Builder
      eName = fromByteString "Message arrived"
      msg'  :: Builder
      msg'  = fromByteString $ TE.encodeUtf8 $ T.toStrict $ msgText msg

Finally, the handler for retrieving the messages from the server can be implemented using evetSourceAppChan, as follows:

eventsH sid = Tagged $ \req respond -> do
    mCh <- lookupChannel env sid
    case mCh of
        Nothing -> do
            let msg = "Could not find session with id: "
                   <> TLE.encodeUtf8 (T.pack (show sid))
            respond $ responseLBS status404 [] msg
        Just ch -> do
            ch' <- dupChan ch
            eventSourceAppChan ch req respond

The full solution is available at my sanbox.

I hope that helps.

Damian Nadales
  • 4,907
  • 1
  • 21
  • 34
  • Hi, curious as to how you might extend your solution to put the Env inside a reader rather than pass explicitly. So if we had `type AppM = ReaderT Env Handler` with most handlers being type `fn :: a -> AppM SomeType` how would you rework eventsH? I've tried `eventsH :: Tagged AppM Application` which compiles, but can't run `env <- ask` in the handler. – esjmb Feb 18 '21 at 11:01
  • 1
    @esjmb you probably want to use Servant.RawM. Your `AppM` is a phantom type in `eventsH` so you don't have access to the `ReaderT`. You'd have to write things in IO and pass in your reader params as in arg otherwise. – mebassett Apr 11 '21 at 23:15
2

Servant can handle this well with just a bit of boilerplate. In this case you need a new content type (EventStream) and a supporting class to render types into SSE format.

{-# LANGUAGE NoImplicitPrelude #-}
module Spencer.Web.Rest.ServerSentEvents where

import RIO
import qualified RIO.ByteString.Lazy as BL
import Servant
import qualified Network.HTTP.Media as M

-- imitate the Servant JSON and OctetStream implementations
data EventStream deriving Typeable

instance Accept EventStream where
  contentType _ = "text" M.// "event-stream"

instance ToSSE a => MimeRender EventStream a where
  mimeRender _ = toSSE

-- imitate the ToJSON type class
class ToSSE a where
  toSSE :: a -> BL.ByteString


-- my custom type with simple SSE render
data Hello = Hello

instance ToSSE Hello where
  toSSE _ = "data: hello!\n\n"

-- my simple SSE server
type MyApi = "sse" :> StreamGet NoFraming EventStream (SourceIO Hello)

myServer :: Server MyAPI
myServer =  return $ source [Hello, Hello, Hello]

Browser result:

data: hello!

data: hello!

data: hello!
goertzenator
  • 1,960
  • 18
  • 28
1

Yeah, I'm not sure about server sent events in servant, but more comprehensive Web frameworks like Yesod has support for that.

Take a look at the package yesod-eventsource

Yesod has pretty nice cookbook so you can event find there pretty nice example

klappvisor
  • 1,099
  • 1
  • 10
  • 28