Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# Revision history for servant-event-stream

## 0.3.1.0 -- 2025-08-21

* Qualify `servant` imports so that `ServerSentEvents` added in `servant-0.20.3.0` does not conflict with ours. (Issue #12)

## 0.3.0.0 -- 2024-09-05

* Breaking changes to the API.

Event streams are implemented using servant's 'Stream' endpoint. You should
provide a handler that returns a stream of events that implements 'ToSourceIO'
where events have a 'ToServerEvent' instance.
provide a handler that returns a stream of events that implements
'ToSourceIO' where events have a 'ToServerEvent' instance.

Example:

Expand Down
8 changes: 4 additions & 4 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
description = "servant-event-stream";

inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-24.05";
nixpkgs.url = "github:nixos/nixpkgs/nixos-25.05";
};

outputs =
Expand Down
4 changes: 2 additions & 2 deletions servant-event-stream.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: >=1.10
name: servant-event-stream
version: 0.3.0.1
version: 0.3.1.0
stability: alpha
synopsis: Servant support for Server-Sent events
category: Servant, Web
Expand All @@ -14,7 +14,7 @@ license: BSD3
license-file: LICENSE
author: Shaun Sharples
maintainer: [email protected]
copyright: (c) 2024 Shaun Sharples
copyright: (c) 2025 Shaun Sharples
build-type: Simple
extra-source-files:
CHANGELOG.md
Expand Down
70 changes: 35 additions & 35 deletions src/Servant/API/EventStream.hs
Original file line number Diff line number Diff line change
Expand Up @@ -68,17 +68,17 @@ import Data.Text (Text)
import Data.Typeable (Typeable)
import GHC.Generics (Generic)
import Network.HTTP.Media ((//), (/:))
import Servant
import Servant.Foreign
import Servant.Foreign.Internal (_FunctionName)
import qualified Servant as S
import qualified Servant.Foreign as S
import qualified Servant.Foreign.Internal as SFI

{- | A ServerSentEvents endpoint emits an event stream using the format described at
<https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format>
-}
data ServerSentEvents (a :: Type)
deriving (Typeable, Generic)

instance HasLink (ServerSentEvents a) where
instance S.HasLink (ServerSentEvents a) where
type MkLink (ServerSentEvents a) r = r
toLink toA _ = toA

Expand All @@ -100,7 +100,7 @@ data ServerEvent = ServerEvent
class ToServerEvent a where
toServerEvent :: a -> ServerEvent

instance (ToServerEvent a) => MimeRender EventStream a where
instance (ToServerEvent a) => S.MimeRender EventStream a where
mimeRender _ = encodeServerEvent . toServerEvent

{- 1. Field names must not contain LF, CR or COLON characters.
Expand All @@ -124,59 +124,59 @@ encodeServerEvent e =
instance ToServerEvent ServerEvent where
toServerEvent = id

instance {-# OVERLAPPABLE #-} (ToServerEvent chunk, ToSourceIO chunk a) => HasServer (ServerSentEvents a) context where
type ServerT (ServerSentEvents a) m = ServerT (StreamGet ServerEventFraming EventStream a) m
route Proxy =
route
(Proxy :: Proxy (StreamGet ServerEventFraming EventStream a))
hoistServerWithContext Proxy =
hoistServerWithContext
(Proxy :: Proxy (StreamGet ServerEventFraming EventStream a))

instance {-# OVERLAPPING #-} (ToServerEvent chunk, ToSourceIO chunk a, GetHeaders (Headers h a)) => HasServer (ServerSentEvents (Headers h a)) context where
type ServerT (ServerSentEvents (Headers h a)) m = ServerT (StreamGet ServerEventFraming EventStream (Headers h a)) m
route Proxy =
route
(Proxy :: Proxy (StreamGet ServerEventFraming EventStream (Headers h a)))
hoistServerWithContext Proxy =
hoistServerWithContext
(Proxy :: Proxy (StreamGet ServerEventFraming EventStream (Headers h a)))
instance {-# OVERLAPPABLE #-} (ToServerEvent chunk, S.ToSourceIO chunk a) => S.HasServer (ServerSentEvents a) context where
type ServerT (ServerSentEvents a) m = S.ServerT (S.StreamGet ServerEventFraming EventStream a) m
route S.Proxy =
S.route
(S.Proxy :: S.Proxy (S.StreamGet ServerEventFraming EventStream a))
hoistServerWithContext S.Proxy =
S.hoistServerWithContext
(S.Proxy :: S.Proxy (S.StreamGet ServerEventFraming EventStream a))

instance {-# OVERLAPPING #-} (ToServerEvent chunk, S.ToSourceIO chunk a, S.GetHeaders (S.Headers h a)) => S.HasServer (ServerSentEvents (S.Headers h a)) context where
type ServerT (ServerSentEvents (S.Headers h a)) m = S.ServerT (S.StreamGet ServerEventFraming EventStream (S.Headers h a)) m
route S.Proxy =
S.route
(S.Proxy :: S.Proxy (S.StreamGet ServerEventFraming EventStream (S.Headers h a)))
hoistServerWithContext S.Proxy =
S.hoistServerWithContext
(S.Proxy :: S.Proxy (S.StreamGet ServerEventFraming EventStream (S.Headers h a)))

-- | a helper instance for <https://hackage.haskell.org/package/servant-foreign-0.15.3/docs/Servant-Foreign.html servant-foreign>
instance
(HasForeignType lang ftype a) =>
HasForeign lang ftype (ServerSentEvents a)
(S.HasForeignType lang ftype a) =>
S.HasForeign lang ftype (ServerSentEvents a)
where
type Foreign ftype (ServerSentEvents a) = Req ftype
type Foreign ftype (ServerSentEvents a) = SFI.Req ftype

foreignFor lang Proxy Proxy req =
foreignFor lang S.Proxy S.Proxy req =
req
& reqFuncName . _FunctionName %~ ("stream" :)
& reqMethod .~ method
& reqReturnType ?~ retType
& SFI.reqFuncName . SFI._FunctionName %~ ("stream" :)
& SFI.reqMethod .~ method
& SFI.reqReturnType ?~ retType
where
retType = typeFor lang (Proxy :: Proxy ftype) (Proxy :: Proxy a)
method = reflectMethod (Proxy :: Proxy 'GET)
retType = SFI.typeFor lang (S.Proxy :: S.Proxy ftype) (S.Proxy :: S.Proxy a)
method = S.reflectMethod (S.Proxy :: S.Proxy S.GET)

{- | A type representation of an event stream. It's responsible for setting proper content-type
and buffering headers, as well as for providing parser implementations for the streams.
Read more on <https://docs.servant.dev/en/stable/tutorial/Server.html#streaming-endpoints Servant Streaming Docs>
-}
data EventStream

instance Accept EventStream where
instance S.Accept EventStream where
contentType _ = "text" // "event-stream" /: ("charset", "utf-8")

-- | Recommended headers for Server-Sent Events.
type RecommendedEventSourceHeaders (a :: Type) = Headers '[Header "X-Accel-Buffering" Text, Header "Cache-Control" Text] a
type RecommendedEventSourceHeaders (a :: Type) = S.Headers '[S.Header "X-Accel-Buffering" Text, S.Header "Cache-Control" Text] a

-- | Add the recommended headers for Server-Sent Events to the response.
recommendedEventSourceHeaders :: a -> RecommendedEventSourceHeaders a
recommendedEventSourceHeaders = addHeader @"X-Accel-Buffering" "no" . addHeader @"Cache-Control" "no-store"
recommendedEventSourceHeaders = S.addHeader @"X-Accel-Buffering" "no" . S.addHeader @"Cache-Control" "no-store"

-- | A framing strategy for Server-Sent Events.
data ServerEventFraming

-- | Frames the server events by joining chunks with a newline.
instance FramingRender ServerEventFraming where
instance S.FramingRender ServerEventFraming where
framingRender _ f = fmap (\x -> f x <> "\n")