reflex-gadt-api-0.2.0.1: src/Reflex/Dom/GadtApi/WebSocket.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module Reflex.Dom.GadtApi.WebSocket
( performWebSocketRequests
, TaggedRequest
, TaggedResponse
, mkTaggedResponse
, WebSocketEndpoint
, tagRequests
) where
import Control.Monad.Fix (MonadFix)
import Data.Constraint.Extras
import Data.Constraint.Forall
import Data.Aeson
import Data.Default
import qualified Data.Text as T
import Data.Typeable
import GHC.Generics
import qualified Data.Map as Map
import Data.Some
import Data.Text (Text)
import Reflex
import Reflex.Dom.Core (Prerender, prerender)
import Reflex.Dom.WebSocket
-- | A request tagged with an identifier
data TaggedRequest = TaggedRequest Int Value
deriving (Typeable, Generic)
instance FromJSON TaggedRequest
instance ToJSON TaggedRequest
-- | A response tagged with an identifier matching the one in the 'TaggedRequest'. The identifier is the first argument.
data TaggedResponse = TaggedResponse Int Value
deriving (Typeable, Generic)
instance FromJSON TaggedResponse
instance ToJSON TaggedResponse
type WebSocketEndpoint = Text
-- | Opens a websockets connection, takes the output of a 'RequesterT' widget
-- and issues that output as API requests over the socket. The result of this
-- function can be fed back into the requester as responses. For example:
--
-- @
-- rec (appResult, requests) <- runRequesterT myApplication responses
-- responses <- performWebSocketRequests myEndpoint requests
-- @
--
performWebSocketRequests
:: forall req js t m.
( Prerender js t m, Applicative m
, FromJSON (Some req)
, forall a. ToJSON (req a)
, ForallF ToJSON req
, Has FromJSON req
)
=> WebSocketEndpoint
-> Event t (RequesterData req)
-> m (Event t (RequesterData (Either Text)))
performWebSocketRequests url req = fmap switchPromptlyDyn $ prerender (pure never) $ do
rec w <- webSocket url $ def
{ _webSocketConfig_send = fmap encode <$> reqs
}
let rsp = fmapMaybe decodeStrict $ _webSocket_recv w
(reqs, rsps) <- tagRequests req rsp
pure rsps
-- | Constructs a response for a given request, and handles the
-- decoding/encoding and tagging steps internal to TaggedRequest and
-- TaggedResponse.
mkTaggedResponse
:: (Monad m, FromJSON (Some f), Has ToJSON f)
=> TaggedRequest
-> (forall a. f a -> m a)
-> m (Either String TaggedResponse)
mkTaggedResponse (TaggedRequest reqId v) f = case fromJSON v of
Success (Some a) -> do
rsp <- f a
pure $ Right $ TaggedResponse reqId (has @ToJSON a $ toJSON rsp)
Error err -> pure $ Left err
-- | This function transforms a request 'Event' into an 'Event' of
-- 'TaggedRequest's (the indexed wire format used to transmit requests). It
-- expects to receive an 'Event' of 'TaggedResponse', the corresponding
-- response wire format, which it will transform into an "untagged" response.
--
-- @
-- requests --> |-------------| --> tagged requests
-- ↗ | | ↘
-- Client | tagRequests | Server
-- ↖ | | ↙
-- responses <-- |-------------| <-- tagged responses
-- @
--
-- This function is provided so that you can use a single websocket for
-- multiple purposes without reimplementing the functionality of
-- 'performWebSocketRequests'. For instance, you might have a websocket split
-- into two "channels," one for these tagged API requests and another for data
-- being pushed from the server.
--
tagRequests
:: forall req t m.
( Applicative m
, FromJSON (Some req)
, forall a. ToJSON (req a)
, ForallF ToJSON req
, Has FromJSON req
, Monad m
, MonadFix m
, Reflex t
, MonadHold t m
)
=> Event t (RequesterData req)
-> Event t TaggedResponse
-> m ( Event t [TaggedRequest]
, Event t (RequesterData (Either Text))
)
tagRequests req rsp = do
rec (matchedReq, matchedRsp) <- matchResponsesWithRequests enc req $
ffor rsp $ \(TaggedResponse t v) -> (t, v)
let wireReq = fmap (Map.elems . Map.mapMaybeWithKey (\t v -> case fromJSON v of
Success (r :: Value) -> Just $ TaggedRequest t r
_ -> Nothing)) matchedReq
pure (wireReq, matchedRsp)
where
enc :: forall a. req a -> (Value, Value -> Either Text a)
enc r =
( whichever @ToJSON @req @a $ toJSON r
, \x -> case has @FromJSON r $ fromJSON x of
Success s-> Right s
Error e -> Left $ T.pack e
)