diff --git a/servant-jsaddle.cabal b/servant-jsaddle.cabal index 0fe89eb..8a36106 100644 --- a/servant-jsaddle.cabal +++ b/servant-jsaddle.cabal @@ -40,8 +40,11 @@ library hs-source-dirs: src ghc-options: -Wall exposed-modules: - Servant.Client.Internal.JSaddleXhrClient Servant.Client.JSaddle + other-modules: + Servant.Client.JSaddle.Internal.Client + Servant.Client.JSaddle.Internal.Fetch + Servant.Client.JSaddle.Internal.Types -- Bundled with GHC: Lower bound to not force re-installs -- text and mtl are bundled starting with GHC-8.4 @@ -71,6 +74,9 @@ library , semigroupoids >=5.3.1 && <6.1 , string-conversions >=0.3 && <0.5 , transformers-base >=0.4.4 && <0.5 + , servant + , mmorph + , kan-extensions if impl(ghc >=8.0) ghc-options: -Wno-redundant-constraints diff --git a/src/Servant/Client/Internal/JSaddleXhrClient.hs b/src/Servant/Client/Internal/JSaddleXhrClient.hs deleted file mode 100644 index d242c85..0000000 --- a/src/Servant/Client/Internal/JSaddleXhrClient.hs +++ /dev/null @@ -1,319 +0,0 @@ -{-# LANGUAGE CPP #-} -{-# LANGUAGE DeriveGeneric #-} -{-# LANGUAGE FlexibleContexts #-} -{-# LANGUAGE FlexibleInstances #-} -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# LANGUAGE MultiParamTypeClasses #-} -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TypeFamilies #-} -{-# LANGUAGE UndecidableInstances #-} -{-# LANGUAGE ViewPatterns #-} -{-# OPTIONS_GHC -fno-warn-name-shadowing #-} - -module Servant.Client.Internal.JSaddleXhrClient where - -import Prelude () -import Prelude.Compat - -import Control.Concurrent - (MVar, newEmptyMVar, takeMVar, tryPutMVar) -import Control.Exception - (Exception, toException) -import Control.Monad - (forM_, unless, void) -import Control.Monad.Catch - (MonadCatch, MonadThrow, catch) -import Control.Monad.Error.Class - (MonadError (..)) -import Control.Monad.IO.Class - (MonadIO (..)) -import Control.Monad.Reader - (MonadReader, ReaderT, asks, runReaderT) -import Control.Monad.Trans.Except - (ExceptT, runExceptT) -import Data.Bifunctor - (bimap, first, second) -import Data.ByteString.Builder - (toLazyByteString) -import qualified Data.ByteString.Char8 as BS -import qualified Data.ByteString.Lazy as BSL -import qualified Data.ByteString.Lazy as L -import Data.CaseInsensitive - (mk, original) -import Data.Char - (isSpace) -import Data.Foldable - (toList) -import Data.Functor.Alt - (Alt (..)) -import Data.Maybe - (fromMaybe) -import Data.Proxy - (Proxy (..)) -import qualified Data.Sequence as Seq -import Data.String.Conversions - (cs) -import qualified Data.Text.Encoding as T -import qualified Data.Text.Encoding.Error as T -import GHC.Generics -import qualified GHCJS.Buffer as Buffer -import qualified GHCJS.DOM -import qualified GHCJS.DOM.EventM as JSDOM -import qualified GHCJS.DOM.Location as Location -import GHCJS.DOM.Types - (DOM, DOMContext, askDOM, runDOM) -import qualified GHCJS.DOM.Types as JS -import qualified GHCJS.DOM.Window as Window -import qualified GHCJS.DOM.XMLHttpRequest as JS -import qualified JavaScript.TypedArray.ArrayBuffer as ArrayBuffer -import qualified Language.Javascript.JSaddle.Types as JSaddle -import Network.HTTP.Media - (renderHeader) -import Network.HTTP.Types - (ResponseHeaders, Status, http11, mkStatus, renderQuery, statusCode) -import System.IO - (hPutStrLn, stderr) - -import Servant.Client.Core - --- Note: assuming encoding UTF-8 - -data ClientEnv - = ClientEnv - { baseUrl :: BaseUrl - -- | Modify the XMLHttpRequest at will, right before sending. - , fixUpXhr :: JS.XMLHttpRequest -> DOM () - } - -data JSaddleConnectionError = JSaddleConnectionError - deriving (Eq, Show) - -instance Exception JSaddleConnectionError - --- | Default 'ClientEnv' -mkClientEnv :: BaseUrl -> ClientEnv -mkClientEnv burl = ClientEnv burl (const (pure ())) - -instance Show ClientEnv where - showsPrec prec (ClientEnv burl _) = - showParen (prec >= 11) - ( showString "ClientEnv {" - . showString "baseUrl = " - . showsPrec 0 burl - . showString ", fixUpXhr = " - . showString "}" - ) - -client :: HasClient ClientM api => Proxy api -> Client ClientM api -client api = api `clientIn` (Proxy :: Proxy ClientM) - -newtype ClientM a = ClientM - { fromClientM :: ReaderT ClientEnv (ExceptT ClientError DOM) a } - deriving ( Functor, Applicative, Monad, MonadIO, Generic - , MonadReader ClientEnv, MonadError ClientError) -deriving instance MonadThrow DOM => MonadThrow ClientM -deriving instance MonadCatch DOM => MonadCatch ClientM - --- | Try clients in order, last error is preserved. -instance Alt ClientM where - a b = a `catchError` const b - -instance RunClient ClientM where - throwClientError = throwError -#if MIN_VERSION_servant_client_core(0,18,1) - runRequestAcceptStatus acceptStatuses r = do - d <- ClientM askDOM - performRequest (fromMaybe [] acceptStatuses) d r -#else - runRequest r = do - d <- ClientM askDOM - performRequest [] d r -#endif - -runClientM :: ClientM a -> ClientEnv -> DOM (Either ClientError a) -runClientM cm env = runExceptT $ flip runReaderT env $ fromClientM cm - -runClientM' :: ClientM a -> DOM (Either ClientError a) -runClientM' m = do - burl <- getDefaultBaseUrl - runClientM m (mkClientEnv burl) - -getDefaultBaseUrl :: DOM BaseUrl -getDefaultBaseUrl = do - win <- GHCJS.DOM.currentWindow >>= \mw -> case mw of - Just x -> pure x - Nothing -> fail "Can not determine default base url without window." - curLoc <- Window.getLocation win - - protocolStr <- Location.getProtocol curLoc - portStr <- Location.getPort curLoc - hostname <- Location.getHostname curLoc - - let protocol - | (protocolStr :: JS.JSString) == "https:" - = Https - | otherwise = Http - - port :: Int - port | null portStr = case protocol of - Http -> 80 - Https -> 443 - | otherwise = read portStr - - pure (BaseUrl protocol hostname port "") - -performRequest :: [Status] -> DOMContext -> Request -> ClientM Response -performRequest acceptStatuses domc req = do - xhr <- JS.newXMLHttpRequest `runDOM` domc - burl <- asks baseUrl - fixUp <- asks fixUpXhr - performXhr xhr burl req fixUp `runDOM` domc - resp <- toResponse domc xhr - - let status = statusCode (responseStatusCode resp) - unless ((status >= 200 && status < 300) || status `elem` (statusCode <$> acceptStatuses)) $ - throwError $ mkFailureResponse burl req resp - - pure resp - - --- * performing requests --- Performs the xhr and blocks until the response was received -performXhr :: JS.XMLHttpRequest -> BaseUrl -> Request -> (JS.XMLHttpRequest -> DOM ()) -> DOM () -performXhr xhr burl request fixUp = do - - let username, password :: Maybe JS.JSString - username = Nothing; password = Nothing - - JS.open xhr (decodeUtf8Lenient $ requestMethod request) (toUrl burl request) True username password - setHeaders xhr request - fixUp xhr - - waiter <- liftIO $ newEmptyMVar - - cleanup <- JSDOM.on xhr JS.readyStateChange $ do - state <- JS.getReadyState xhr - case state of - -- onReadyStateChange's callback can fire state 4 - -- (which means "request finished and response is ready") - -- multiple times. By using tryPutMVar, only the first time - -- state 4 is fired will cause an MVar to be put. Subsequent - -- fires are ignored. - 4 -> void $ liftIO $ tryPutMVar waiter () - _ -> return () - - sendXhr xhr (toBody request) `catch` handleXHRError waiter -- We handle any errors in `toResponse`. - - liftIO $ takeMVar waiter - - cleanup - - where - - handleXHRError :: MVar () -> JS.XHRError -> DOM () - handleXHRError waiter e = do - liftIO $ hPutStrLn stderr $ "servant-client-jsaddle: exception in `sendXhr` (should get handled in response handling): " <> show e - void $ liftIO $ tryPutMVar waiter () - - -toUrl :: BaseUrl -> Request -> JS.JSString -toUrl burl request = - let pathS = JS.toJSString $ decodeUtf8Lenient $ L.toStrict $ toLazyByteString $ - requestPath request - queryS = - JS.toJSString $ decodeUtf8Lenient $ - renderQuery True $ - toList $ - requestQueryString request - in JS.toJSString (showBaseUrl burl) <> pathS <> queryS :: JS.JSString - -setHeaders :: JS.XMLHttpRequest -> Request -> DOM () -setHeaders xhr request = do - forM_ (toList $ requestAccept request) $ \mediaType -> -- FIXME review - JS.setRequestHeader - xhr - ("Accept" :: JS.JSString) - (decodeUtf8Lenient $ renderHeader mediaType) - - forM_ (requestBody request) $ \(_, mediaType) -> - JS.setRequestHeader - xhr - ("Content-Type" :: JS.JSString) - (decodeUtf8Lenient $ renderHeader mediaType) - - forM_ (toList $ requestHeaders request) $ \(key, value) -> - JS.setRequestHeader xhr (decodeUtf8Lenient $ original key) (decodeUtf8Lenient value) - --- ArrayBufferView is a type that only exists in the spec and covers many concrete types. -castMutableArrayBufferToArrayBufferView :: ArrayBuffer.MutableArrayBuffer -> DOM JS.ArrayBufferView -castMutableArrayBufferToArrayBufferView x = JS.liftJSM $ do - JS.fromJSValUnchecked $ JS.pToJSVal x - -mkFailureResponse :: BaseUrl -> Request -> ResponseF BSL.ByteString -> ClientError -mkFailureResponse burl request = - FailureResponse (bimap (const ()) f request) - where - f b = (burl, BSL.toStrict $ toLazyByteString b) - -sendXhr :: JS.XMLHttpRequest -> Maybe L.ByteString -> DOM () -sendXhr xhr Nothing = JS.send xhr -sendXhr xhr (Just body) = do - -- Reason for copy: hopefully offset will be 0 and length b == len - -- FIXME: use a typed array constructor that accepts offset and length and skip the copy - (b, _offset, _len) <- JSaddle.ghcjsPure $ Buffer.fromByteString $ BS.copy $ L.toStrict body - b' <- Buffer.thaw b - b'' <- JSaddle.ghcjsPure $ Buffer.getArrayBuffer b' - JS.sendArrayBuffer xhr =<< castMutableArrayBufferToArrayBufferView b'' - -toBody :: Request -> Maybe L.ByteString -toBody request = case requestBody request of - Nothing -> Nothing - Just (RequestBodyLBS "", _) -> Nothing - Just (RequestBodyLBS x, _) -> Just x - Just (RequestBodyBS "", _) -> Nothing - Just (RequestBodyBS x, _) -> Just $ L.fromStrict x - Just (RequestBodySource _, _) -> error "RequestBodySource isn't supported" - --- * inspecting the xhr response - --- This function is only supposed to handle 'ConnectionError's. Other --- 'ClientError's are created in Servant.Client.Req. -toResponse :: DOMContext -> JS.XMLHttpRequest -> ClientM Response -toResponse domc xhr = do - let inDom :: DOM a -> ClientM a - inDom = flip runDOM domc - status <- inDom $ JS.getStatus xhr - case status of - 0 -> throwError $ ConnectionError $ toException JSaddleConnectionError - _ -> inDom $ do - statusText <- BS.pack <$> JS.getStatusText xhr - headers <- parseHeaders <$> JS.getAllResponseHeaders xhr - responseText <- maybe "" (L.fromStrict . BS.pack) <$> JS.getResponseText xhr -- FIXME: Text/Binary? Performance? Test? - pure Response - { responseStatusCode = mkStatus (fromIntegral status) statusText - , responseBody = responseText - , responseHeaders = Seq.fromList headers - , responseHttpVersion = http11 -- this is made up - } - -parseHeaders :: String -> ResponseHeaders -parseHeaders s = - (first mk . first strip . second strip . parseHeader) <$> - splitOn "\r\n" (cs s) - where - parseHeader :: BS.ByteString -> (BS.ByteString, BS.ByteString) - parseHeader h = case BS.breakSubstring ":" (cs h) of - (key, BS.drop 1 -> value) -> (key, value) - - splitOn :: BS.ByteString -> BS.ByteString -> [BS.ByteString] - splitOn separator input = case BS.breakSubstring separator input of - (prefix, "") -> [prefix] - (prefix, rest) -> prefix : splitOn separator (BS.drop (BS.length separator) rest) - - strip :: BS.ByteString -> BS.ByteString - strip = BS.dropWhile isSpace . BS.reverse . BS.dropWhile isSpace . BS.reverse - -decodeUtf8Lenient :: BS.ByteString -> JS.JSString -decodeUtf8Lenient = JS.toJSString . T.decodeUtf8With T.lenientDecode diff --git a/src/Servant/Client/JSaddle.hs b/src/Servant/Client/JSaddle.hs index f3a65ce..500b218 100644 --- a/src/Servant/Client/JSaddle.hs +++ b/src/Servant/Client/JSaddle.hs @@ -7,6 +7,7 @@ module Servant.Client.JSaddle , ClientM , runClientM , runClientM' + , withClientM -- * Configuration , ClientEnv(..) @@ -16,5 +17,6 @@ module Servant.Client.JSaddle , module Servant.Client.Core.Reexport ) where -import Servant.Client.Internal.JSaddleXhrClient +import Servant.Client.JSaddle.Internal.Client +import Servant.Client.JSaddle.Internal.Types import Servant.Client.Core.Reexport diff --git a/src/Servant/Client/JSaddle/Internal/Client.hs b/src/Servant/Client/JSaddle/Internal/Client.hs new file mode 100644 index 0000000..ee8575b --- /dev/null +++ b/src/Servant/Client/JSaddle/Internal/Client.hs @@ -0,0 +1,135 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE OverloadedStrings #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} + +module Servant.Client.JSaddle.Internal.Client where + +import Prelude () +import Prelude.Compat + +import Control.Exception + (evaluate) +import Control.Monad + (unless) +import Control.Monad.Codensity + (Codensity(..)) +import Control.Monad.Error.Class + (MonadError (..)) +import Control.Monad.IO.Class + (MonadIO (..)) +import Control.Monad.Reader + (asks, runReaderT) +import Control.Monad.Trans.Except + (runExceptT) +import Data.Bifunctor + (bimap) +import Data.ByteString.Builder + (toLazyByteString) +import qualified Data.ByteString.Lazy as BSL +import Data.Maybe + (fromMaybe) +import Data.Proxy + (Proxy (..)) +import qualified GHCJS.DOM +import qualified GHCJS.DOM.Location as Location +import GHCJS.DOM.Types + (DOM, DOMContext, askDOM, runDOM) +import qualified GHCJS.DOM.Types as JS +import qualified GHCJS.DOM.Window as Window +import qualified Language.Javascript.JSaddle as JSaddle +import Network.HTTP.Types + (Status, statusCode) + +import Servant.Client.Core +import Servant.Client.JSaddle.Internal.Fetch +import Servant.Client.JSaddle.Internal.Types + +-- | Default 'ClientEnv' +mkClientEnv :: BaseUrl -> ClientEnv +mkClientEnv burl = ClientEnv burl ((\(JS.Object o) -> JS.RequestInit o) <$> JSaddle.obj) + +client :: HasClient ClientM api => Proxy api -> Client ClientM api +client api = api `clientIn` (Proxy :: Proxy ClientM) + +instance RunClient ClientM where + throwClientError = throwError +#if MIN_VERSION_servant_client_core(0,18,1) + runRequestAcceptStatus acceptStatuses r = do + d <- ClientM askDOM + performRequest (fromMaybe [] acceptStatuses) d r +#else + runRequest r = do + d <- ClientM askDOM + performRequest [] d r +#endif + +instance RunStreamingClient ClientM where + withStreamingRequest req k = do + d <- ClientM askDOM + performWithStreamingRequest d req k + +runClientM :: ClientM a -> ClientEnv -> DOM (Either ClientError a) +runClientM cm env = withClientM cm env (liftIO . evaluate) + +runClientM' :: ClientM a -> DOM (Either ClientError a) +runClientM' m = do + burl <- getDefaultBaseUrl + runClientM m (mkClientEnv burl) + +withClientM :: ClientM a -> ClientEnv -> (Either ClientError a -> DOM b) -> DOM b +withClientM cm env k = + let Codensity f = runExceptT $ flip runReaderT env $ fromClientM cm + in f k + +getDefaultBaseUrl :: DOM BaseUrl +getDefaultBaseUrl = do + win <- GHCJS.DOM.currentWindow >>= \mw -> case mw of + Just x -> pure x + Nothing -> fail "Can not determine default base url without window." + curLoc <- Window.getLocation win + + protocolStr <- Location.getProtocol curLoc + portStr <- Location.getPort curLoc + hostname <- Location.getHostname curLoc + + let protocol + | (protocolStr :: JS.JSString) == "https:" + = Https + | otherwise = Http + + port :: Int + port | null portStr = case protocol of + Http -> 80 + Https -> 443 + | otherwise = read portStr + + pure (BaseUrl protocol hostname port "") + +performRequest :: [Status] -> DOMContext -> Request -> ClientM Response +performRequest acceptStatuses domc req = do + burl <- asks baseUrl + rinit <- asks requestInit >>= flip runDOM domc + performFetch req burl rinit `runDOM` domc >>= + either throwClientError + (\fetch -> do + resp <- toResponse domc fetch + + let status = statusCode (responseStatusCode resp) + unless ((status >= 200 && status < 300) || status `elem` (statusCode <$> acceptStatuses)) $ + throwClientError $ mkFailureResponse burl req resp + + pure resp + ) + +performWithStreamingRequest :: DOMContext -> Request -> (StreamingResponse -> IO a) -> ClientM a +performWithStreamingRequest domc req k = do + burl <- asks baseUrl + rinit <- asks requestInit >>= flip runDOM domc + performStreamingRequest domc req burl rinit k + +mkFailureResponse :: BaseUrl -> Request -> ResponseF BSL.ByteString -> ClientError +mkFailureResponse burl request = + FailureResponse (bimap (const ()) f request) + where + f b = (burl, BSL.toStrict $ toLazyByteString b) diff --git a/src/Servant/Client/JSaddle/Internal/Fetch.hs b/src/Servant/Client/JSaddle/Internal/Fetch.hs new file mode 100644 index 0000000..bca7a77 --- /dev/null +++ b/src/Servant/Client/JSaddle/Internal/Fetch.hs @@ -0,0 +1,201 @@ +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeApplications #-} + +module Servant.Client.JSaddle.Internal.Fetch where + +import Prelude () +import Prelude.Compat hiding ((!!)) + +import Control.Exception + (toException, throwIO) +import Control.Monad + ((<=<), void) +import Control.Monad.Catch + (try) +import Control.Monad.Codensity +import Control.Monad.Error.Class + (MonadError (..)) +import Control.Monad.IO.Class + (MonadIO (..)) +import Control.Monad.Morph +import Data.ByteString.Builder + (toLazyByteString) +import Data.Bifunctor + (bimap) +import qualified Data.ByteString.Char8 as BS +import qualified Data.ByteString.Lazy as L +import Data.CaseInsensitive + (mk, original) +import Data.Foldable +import Data.Maybe + (fromMaybe) +import qualified Data.Sequence as Seq +import Data.Text.Encoding +import qualified JavaScript.TypedArray.ArrayBuffer as AB +import qualified GHCJS.Buffer +import GHCJS.DOM.Types +import Network.HTTP.Media +import Network.HTTP.Types + (http11, mkStatus, renderQuery) +import Language.Javascript.JSaddle + +import Servant.Client.Core hiding (Request, Response) +import qualified Servant.Client.Core as Servant +import Servant.Client.JSaddle.Internal.Types +import qualified Servant.Types.SourceT as S + +performFetch :: Servant.Request -> BaseUrl -> RequestInit -> JSM (Either ClientError Response) +performFetch req burl rinit = do + let url = toUrl burl req + -- TODO use headers from rinit and amend them instead, if available + headers <- new (jsg ("Headers" :: JSString)) () + + forM_ (toList $ requestAccept req) $ \mediaType -> + headers # ("append" :: JSString) $ + ( "Accept" :: JSString + , decodeUtf8Lenient $ renderHeader mediaType + ) + + forM_ (toList $ requestHeaders req) $ \(key, value) -> + headers # ("append" :: JSString) $ + ( decodeUtf8Lenient $ original key + , decodeUtf8Lenient value + ) + + let setTextBody x = + rinit <# ("body" :: JSString) $ decodeUtf8Lenient x + setContentType t = void $ headers # ("append" :: JSString) $ + ( "Content-Type" :: JSString + , decodeUtf8Lenient $ renderHeader t + ) + + setBody x t = do + case (mainType t, subType t) of + ("application", "json") -> setTextBody x + ("text", _) -> setTextBody x + ("image", "svg+xml") -> setTextBody x + _ -> do + x' <- bsToJSVal $ L.fromStrict x + rinit <# ("body" :: JSString) $ x' + + stream writer s = do + let u = S.unSourceT s + consume S.Stop = pure $ Left Nothing + consume (S.Error e) = pure $ Left $ Just e + consume (S.Skip step) = consume step + consume (S.Yield x step) = pure $ Right (x, step) + consume (S.Effect a) = a >>= consume + res <- liftIO $ u consume + case res of + Left Nothing -> () <$ (writer # ("stop" :: JSString) $ ()) + Left (Just e) -> () <$ (writer # ("abort" :: JSString) $ e) + Right (x, step) -> do + x' <- bsToJSVal x + _ <- readPromise =<< (writer # ("write" :: JSString) $ x') + stream writer $ S.fromStepT step + + case requestBody req of + Nothing -> pure () + Just (RequestBodyLBS "", _) -> pure () + Just (RequestBodyLBS x, t) -> + setContentType t >> setBody (L.toStrict x) t + Just (RequestBodyBS "", _) -> pure () + Just (RequestBodyBS x, t) -> + setContentType t >> setBody x t + Just (RequestBodySource x, t) -> do + writer <- do + setContentType t + transformStream <- new (jsg ("TransformStream" :: JSString)) () + readable <- transformStream ! ("readable" :: JSString) + rinit <# ("body" :: JSString) $ readable + writable <- transformStream ! ("writable" :: JSString) + writable # ("getWriter" :: JSString) $ () + stream writer x + pure () + + rinit <# ("headers" :: JSString) $ headers + rinit <# ("method" :: JSString) $ decodeUtf8Lenient $ requestMethod req + + bimap (ConnectionError . toException @PromiseRejected) Response <$> + try (readPromise =<< jsg2 ("fetch" :: JSString) url rinit) + + where + bsToJSVal x = do + (x',_,_) <- ghcjsPure (GHCJS.Buffer.fromByteString $ L.toStrict x) + pToJSVal <$> (ghcjsPure . GHCJS.Buffer.getArrayBuffer =<< GHCJS.Buffer.thaw x') + +toUrl :: BaseUrl -> Servant.Request -> JSString +toUrl burl request = + let pathS = toJSString $ decodeUtf8Lenient $ L.toStrict $ toLazyByteString $ + requestPath request + queryS = + toJSString $ decodeUtf8Lenient $ + renderQuery True $ + toList $ + requestQueryString request + in toJSString (showBaseUrl burl) <> pathS <> queryS :: JSString + +toResponseGeneric :: (MakeObject t, ToJSVal t) => t -> JSM a -> JSM (Either ClientError (ResponseF a)) +toResponseGeneric response getBody = do + status <- read @Int . fromJSString <$> (valToStr =<< (response ! ("status" :: JSString))) + case status of + 0 -> pure $ Left $ ConnectionError $ toException JSaddleConnectionError + _ -> do + statusText <- BS.pack . fromJSString <$> + (valToStr =<< response ! ("statusText" :: JSString)) + headersField <- response ! ("headers" :: JSString) + iterator <- headersField # ("entries" :: JSString) $ () + headers <- getHeaders iterator + body <- getBody + pure $ Right $ Servant.Response + { responseStatusCode = mkStatus status statusText + , responseBody = body + , responseHeaders = Seq.fromList headers + , responseHttpVersion = http11 + } + where + getHeaders iterator = do + res <- iterator # ("next" :: JSString) $ () + done <- fromMaybe False <$> (nullableToMaybe =<< res ! ("done" :: JSString)) + if done then return [] else do + x <- res ! ("value" :: JSString) + k <- BS.pack . fromJSString <$> (valToStr =<< x !! 0) + v <- BS.pack . fromJSString <$> (valToStr =<< x !! 1) + ((mk k,v):) <$> getHeaders iterator + +toResponse :: DOMContext -> Response -> ClientM Servant.Response +toResponse domc response = do + either throwError pure =<< + (flip runDOM domc $ toResponseGeneric response $ + (pFromJSVal <$> (response # ("arrayBuffer" :: JSString) $ ())) >>= + (fmap pFromJSVal . readPromise) >>= + AB.unsafeFreeze >>= + ghcjsPure . GHCJS.Buffer.createFromArrayBuffer >>= + fmap L.fromStrict . ghcjsPure . (GHCJS.Buffer.toByteString 0 Nothing)) + +performStreamingRequest :: DOMContext -> Servant.Request -> BaseUrl -> RequestInit -> (StreamingResponse -> IO a) -> ClientM a +performStreamingRequest domc req burl rinit k = do + ClientM $ lift $ lift $ Codensity $ \k1 -> bracket (performFetch req burl rinit) + (either (const $ pure ()) (fmap (const ()) . closeFetch)) $ + either (liftIO . throwIO) $ \response -> + bracket (response ! ("body" :: JSString) >>= \body -> body # ("getReader" :: JSString) $ ()) + (\reader -> reader # ("releaseLock" :: JSString) $ ()) $ \reader -> do + let steps = flip runDOM domc $ do + chunk <- readPromise =<< (reader # ("read" :: JSString) $ ()) + done <- fromMaybe False <$> (nullableToMaybe =<< (chunk ! ("done" :: JSString))) + if done + then pure $ S.Stop + else do + value <- chunk ! ("value" :: JSString) >>= + (! ("buffer" :: JSString)) >>= + AB.unsafeFreeze . pFromJSVal >>= + ghcjsPure . GHCJS.Buffer.createFromArrayBuffer >>= + ghcjsPure . (GHCJS.Buffer.toByteString 0 Nothing) + pure $ S.Yield value $ S.Effect $ liftIO steps + + either (liftIO . throwIO) (k1 <=< liftIO . k) =<< + toResponseGeneric response (pure $ S.SourceT (=<< steps)) + where + closeFetch response = do + body <- response ! ("body" :: JSString) + body # ("cancel" :: JSString) $ () diff --git a/src/Servant/Client/JSaddle/Internal/Types.hs b/src/Servant/Client/JSaddle/Internal/Types.hs new file mode 100644 index 0000000..a02ac06 --- /dev/null +++ b/src/Servant/Client/JSaddle/Internal/Types.hs @@ -0,0 +1,57 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# OPTIONS_GHC -fno-warn-name-shadowing #-} + +module Servant.Client.JSaddle.Internal.Types where + +import Prelude () +import Prelude.Compat + +import Control.Monad.Codensity +import Control.Exception + (Exception) +import Control.Monad.Error.Class + (MonadError (..)) +import Control.Monad.IO.Class + (MonadIO (..)) +import Control.Monad.Reader + (MonadReader, ReaderT) +import Control.Monad.Trans.Except + (ExceptT) +import Data.Functor.Alt + (Alt (..)) +import GHC.Generics +import GHCJS.DOM.Types + +import Servant.Client.Core + +-- Note: assuming encoding UTF-8 +data ClientEnv + = ClientEnv + { baseUrl :: BaseUrl + , requestInit :: JSM RequestInit + } + +data JSaddleConnectionError = JSaddleConnectionError + deriving (Eq, Show) + +instance Exception JSaddleConnectionError + +instance Show ClientEnv where + showsPrec prec (ClientEnv burl _) = + showParen (prec >= 11) + ( showString "ClientEnv {" + . showString "baseUrl = " + . showsPrec 0 burl + . showString ", requestInit = " + . showString "}" + ) + +newtype ClientM a = ClientM + { fromClientM :: ReaderT ClientEnv (ExceptT ClientError (Codensity DOM)) a } + deriving ( Functor, Applicative, Monad, MonadIO, Generic + , MonadReader ClientEnv, MonadError ClientError) + +-- | Try clients in order, last error is preserved. +instance Alt ClientM where + a b = a `catchError` const b