aws-transcribe-ws-0.0.1.0: src/AWS/Transcribe/Client.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module AWS.Transcribe.Client (runClient) where
import AWS.Credentials (Credentials)
import AWS.Transcribe.Channel (Channel (MkChannel))
import AWS.Transcribe.EventStream (hEventType, hValueString, mkStreamingMessage, payload)
import qualified AWS.Transcribe.PreSignedUrl as PS
import AWS.Transcribe.Settings
import AWS.Transcribe.StreamingResponse (StreamingError (..), StreamingResponse (EndOfStream, Error, Event))
import Control.Concurrent.Async (Async, cancel, poll, withAsync)
import Control.Concurrent.STM (TQueue, atomically, readTQueue, writeTQueue)
import Control.Lens ((^.))
import qualified Data.Aeson as AE
import Data.Binary (decode, encode)
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Time (getCurrentTime)
import qualified Network.WebSockets as WS
import qualified Wuss as WS
{- | Start a @WebSocket@ client.
The client will probe the channel for incoming `BL.ByteString`, encode it in
the expected @Event Stream@ and transmit them to the AWS Transcribe endpoint.
When a response is received it will be written in the channel.
Sending an empty `BL.ByteString` will close the @WebSocket@ connection. Receiving any kind
of exception from @AWS Transcribe@ will also close the connection.
-}
runClient ::
-- | AWS Credentials
Credentials ->
-- | Session settings
Settings ->
-- | Transcribe channel
Channel ->
IO ()
runClient creds settings (MkChannel wQ rQ) = do
putStrLn "start client"
now <- getCurrentTime
WS.runSecureClient
(T.unpack $ PS.host $ region settings)
8443
(T.unpack $ T.decodeUtf8 $ PS.path creds settings now)
$ \conn ->
withAsync (send conn wQ) $ \handle ->
receive conn rQ handle
send :: WS.Connection -> TQueue BL.ByteString -> IO ()
send conn wQ = go
where
go = do
pl <- atomically $ readTQueue wQ
WS.sendBinaryData conn $ encode $ mkStreamingMessage $ BL.toStrict pl
-- Sending an empty message will shut down the socket
if BL.null pl then pure () else go
receive :: WS.Connection -> TQueue StreamingResponse -> Async () -> IO ()
receive conn rQ handle = go
where
go = withRunningSend rQ handle $ do
message <- WS.receive conn
case message of
WS.DataMessage _ _ _ (WS.Binary msg) -> do
-- TODO: Use decodeOrFail
let decoded = decode msg
let response = case decoded ^. hEventType . hValueString of
"BadRequestException" -> Error BadRequestException
"InternalFailureException" -> Error InternalFailureException
"LimitExceededException" -> Error LimitExceededException
"UnrecognizedClientException" -> Error UnrecognizedClientException
"TranscriptEvent" ->
case AE.eitherDecodeStrict $ decoded ^. payload of
Left err -> Error $ TranscriptEventError err
Right trans -> Event trans
hOther -> Error $ OtherStreamingError decoded $ "Unknown header: " <> T.unpack hOther
atomically $ writeTQueue rQ response
go
-- On close, clean up the send async action and close the connection
WS.ControlMessage (WS.Close _ _) -> cancel handle >> atomically (writeTQueue rQ EndOfStream) >> pure ()
-- Ignore other messages
_ -> go
-- Perform an action while the async send has not raised an exception. If an
-- exception is raised in the send thread, a `EndOfStream` message is sent
-- to the stream and the action is not run.
withRunningSend :: TQueue StreamingResponse -> Async () -> IO () -> IO ()
withRunningSend rQ handle action = do
status <- poll handle
case status of
-- If the sendthread returns, we still run the receive action
-- to catch any in-flight messages
Just (Right _) -> action
Just (Left _) -> atomically (writeTQueue rQ EndOfStream) >> pure ()
Nothing -> action