packages feed

reflex-gadt-api-0.4.0.0: src/Reflex/Dom/GadtApi/XHR.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuantifiedConstraints #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Reflex.Dom.GadtApi.XHR where

import Control.Concurrent (forkIO, newEmptyMVar, putMVar, takeMVar)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Aeson
import Data.Constraint.Extras (Has, has)
import Data.Default (Default (..))
import Data.Functor (void)
import Data.Text (Text)
import Language.Javascript.JSaddle (MonadJSM)
import Language.Javascript.JSaddle.Monad (runJSM, askJSM)
import Reflex.Dom.Core

type ApiEndpoint = Text

-- | Configuration for 'performXhrRequests' and 'apiRequestXhr'.
--
-- 'Reflex.Dom.Xhr.postJson' builds each request as a JSON @POST@. Use
-- '_xhrClientConfig_configureRequest' to adjust the
-- 'Reflex.Dom.Xhr.XhrRequest' before the library sends it. For example, add
-- headers or credentials.
data XhrClientConfig = XhrClientConfig
  { _xhrClientConfig_url :: ApiEndpoint
    -- ^ URL that the requests are sent to
  , _xhrClientConfig_configureRequest :: XhrRequest Text -> XhrRequest Text
    -- ^ Adjust each outgoing 'XhrRequest' before it is sent
  }

-- | The default configuration uses the empty URL and does not change the
-- requests. Set '_xhrClientConfig_url' before you use it.
instance Default XhrClientConfig where
  def = XhrClientConfig
    { _xhrClientConfig_url = ""
    , _xhrClientConfig_configureRequest = id
    }

-- | Send the output of a 'RequesterT' widget as API requests. Feed the
-- result back into the requester as responses. For example:
--
-- @
-- rec (appResult, requests) <- runRequesterT myApplication responses
--     responses <- performXhrRequests (def { _xhrClientConfig_url = myApiEndpoint }) requests
-- @

performXhrRequests
  :: forall t m api.
     ( Has FromJSON api
     , forall a. ToJSON (api a)
     , Prerender t m
     , Applicative m
     )
  => XhrClientConfig
  -> Event t (RequesterData api)
  -> m (Event t (RequesterData (Either XhrError)))
performXhrRequests cfg req = fmap switchPromptlyDyn $ prerender (pure never) $ do
  performEventAsync $ ffor req $ \r yield -> do
    ctx <- askJSM
    void $ liftIO $ forkIO $ flip runJSM ctx $
      liftIO . yield =<< apiRequestXhr cfg r

-- | A failed request. The record holds the request as it was sent. It also
-- holds the response, or the 'Reflex.Dom.Xhr.XhrException' that stopped the
-- request. 'XhrException_Aborted' shows an aborted request.
-- 'XhrException_Error' shows a request that could not be sent.
data XhrError = XhrError
  { _xhrError_request :: XhrRequest Text
  , _xhrError_response :: Either XhrException XhrResponse
  }

xhrErrorToText :: XhrError -> Text
xhrErrorToText e =
  let url = _xhrRequest_url $ _xhrError_request e
  in case _xhrError_response e of
    Left XhrException_Aborted -> "Aborted: " <> url
    Left XhrException_Error -> "Request error: " <> url
    Right rsp ->
      let
        status = _xhrResponse_statusText rsp
        body = _xhrResponse_responseText rsp
      in status <> case body of
        Nothing -> ""
        Just b -> ": " <> b

-- | Encode an API request as JSON. Send it with one 'XhrRequest'. Decode
-- the response.
--
-- Only a response that does not decode gives an 'XhrError'. This function
-- does not check the HTTP status of the response. A non-2xx response with a
-- body that decodes is a success. Read '_xhrError_response' to get the
-- status of a failed request.
apiRequestXhr
  :: forall api m.
     ( MonadIO m
     , MonadJSM m
     , Has FromJSON api
     , forall a. ToJSON (api a)
     )
  => XhrClientConfig
  -> RequesterData api
  -> m (RequesterData (Either XhrError))
apiRequestXhr cfg = traverseRequesterData $ \x ->
  has @FromJSON @api x $ mkRequest x
  where
    mkRequest
      :: (MonadJSM m, FromJSON b)
      => api b
      -> m (Either XhrError b)
    mkRequest req = do
      response <- liftIO newEmptyMVar
      let request = _xhrClientConfig_configureRequest cfg $
            postJson (_xhrClientConfig_url cfg) req
      _ <- newXMLHttpRequestWithError request $ liftIO . putMVar response
      result <- liftIO $ takeMVar response
      pure $ case result of
        Left e -> Left $ XhrError request (Left e)
        Right xhrResp -> case decodeXhrResponse xhrResp of
          Nothing -> Left $ XhrError request (Right xhrResp)
          Just r -> Right r