Skip to content

Conversation

voidus
Copy link
Contributor

@voidus voidus commented Mar 14, 2025

Picking up #1317.

I rebased the changes to be a single commit after adding the fix on top, but there's also the unrebased version: master...voidus:servant:feature/server-sent-events-unrebased

Reproducing the old PR description below:


This PR is an attempt to implement SSE for the Servant client-side.


Small example

import qualified Data.Aeson                           as Aeson
import           Network.HTTP.Client.TLS
import           Servant.API
import           Servant.Client.Core.ServerSentEvents (JsonEventStreamT (..))
import           Servant.Client.Streaming
import           Servant.Types.SourceT                (foreachStep)

data NewsEntry

instance Show NewsEntry

instance Aeson.FromJSON NewsEntry

type NewsAPI = "news-stream" :> ServerSentEvents 'JsonEvent [NewsEntry]

newsAPI :: ClientM (JsonEventStreamT IO [NewsEntry])
newsAPI = client (Proxy @NewsAPI)

main :: IO ()
main = do
  mgr <- newTlsManager
  let env = mkClientEnv mgr $ BaseUrl Https "api.example.com" 443 "stable"
  withClientM newsAPI env $ either print $
    foreachStep fail print . unJsonEventStreamT

Copy link
Contributor

@tchoutri tchoutri left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @voidus! Provided CI is happy, I'll merge.

@tchoutri
Copy link
Contributor

@voidus This is not a blocker for this PR, but would you be interested to write a cookbook to showcase this feature?

@voidus
Copy link
Contributor Author

voidus commented Mar 14, 2025

Happy to contribute!

A cookbook entry sounds great, I'll try to open a PR for that in the next days.

@tchoutri tchoutri merged commit d4625a3 into haskell-servant:master Mar 14, 2025
7 checks passed
@tchoutri tchoutri moved this to Done in Servant 0.20.3.0 Mar 14, 2025
@voidus voidus deleted the feature/server-sent-events branch March 17, 2025 02:50
@freckletonj
Copy link

It took me a bit to figure out the non-JSON version, so in case it helps anyone:

import Network.HTTP.Client.TLS ( newTlsManager )
import Servant.API
    ( ServerSentEvents, EventKind(RawEvent) )
import Servant.Client.Core.ServerSentEvents (Event (Event), EventStreamT (unEventStreamT))
import Servant.Client.Streaming
    ( ClientM,
      BaseUrl(BaseUrl),
      Scheme(Http),
      client,
      mkClientEnv,
      withClientM )
import Servant.Types.SourceT (StepT (..), SourceT (unSourceT))
import Data.Data (Proxy(..))
import GHC.IO.Handle (hFlush)
import GHC.IO.Handle.FD (stdout)
import qualified Data.ByteString.Char8 as BS

type MyAPI = ServerSentEvents 'RawEvent (Event BS.ByteString)

newsAPI :: ClientM (EventStreamT IO)
newsAPI = client (Proxy @MyAPI)

main :: IO ()
main = do
  mgr <- newTlsManager
  let env = mkClientEnv mgr $ BaseUrl Http "localhost" 8080 ""
  withClientM newsAPI env $ \case
    Left err -> (print err)
    Right stream -> do
       putStrLn "operating on stream obj"
       -- let sourceT = unJsonEventStreamT stream
       let sourceT = unEventStreamT stream
       unSourceT sourceT go
        where
          go :: StepT IO (Event BS.ByteString) -> IO ()
          go Stop = return ()
          go (Error err) = do
            putStrLn "error"
            print err
          go (Skip s) = do
            putStrLn "skip"
            go s
          go (Effect ms) = do
            putStrLn "effect"
            result <- ms
            go result
          go (Yield (Event name dat) s) = do
            putStrLn $ "yield: " ++ BS.unpack dat
            hFlush stdout
            go s

@bflyblue
Copy link

Any ideas on how the SSE client and https://hackage.haskell.org/package/servant-event-stream should work together?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

5 participants