packages feed

http2-5.4.4: Network/HTTP2/H2/OutBodyIface.hs

{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}

module Network.HTTP2.H2.OutBodyIface (
    StreamTerminated (..),
    withOutBodyIface,
) where

import Control.Concurrent.STM
import Control.Exception
import Network.HTTP.Semantics
import Network.HTTP.Semantics.IO
import Network.HTTP2.H2.Context
import Network.HTTP2.H2.Sync
import Network.HTTP2.H2.Types

----------------------------------------------------------------

data StreamTerminated
    = StreamPushedFinal
    | StreamCancelled
    | StreamOutOfScope
    | StreamRemoteReset ClosedCode
    deriving (Show)
    deriving anyclass (Exception)

----------------------------------------------------------------

withOutBodyIface
    :: Context
    -> Stream
    -> TBQueue StreamingChunk
    -> (forall a. IO a -> IO a)
    -> (OutBodyIface -> IO r)
    -> IO r
withOutBodyIface ctx@Context{outputQ} strm tbq unmask k = do
    terminated <- newTVarIO Nothing
    let checkNotTerminated :: STM ()
        checkNotTerminated = do
            mTerminated <- readTVar terminated
            maybe (return ()) throwSTM mTerminated

        -- Check if the peer is still listening for messages
        --
        -- It is important to call 'checkNotClosed' prior to enqueuing stream
        -- chunks to ensure that 'writeTBQueue' will not block indefinitely
        -- (because nothing is consuming elements from the queue anymore).
        --
        -- Assumes 'checkNotTerminated'.
        checkNotClosed :: STM ()
        checkNotClosed = do
            mClosed <- getIsClosed
            case mClosed of
                Just code ->
                    -- When the stream is closed, but /we/ did not close it (or
                    -- 'checkNotTerminated' would have thrown an exception), it
                    -- must mean that our peer send us a RST_STREAM, indicating
                    -- that they do not want to receive any further messages.
                    throwSTM $ StreamRemoteReset code
                _otherwise ->
                    return ()

        getIsClosed :: STM (Maybe ClosedCode)
        getIsClosed = do
            st <- readTVar (streamState strm)
            case st of
                Closed code -> return $ Just code
                _otherwise -> return Nothing

        cancelAfterFinish :: Maybe SomeException -> STM ()
        cancelAfterFinish mErr =
            writeTQueue outputQ $ makeOutputIO ctx strm (OReset mErr)

        iface :: OutBodyIface
        iface =
            OutBodyIface
                { outBodyUnmask = unmask
                , outBodyPush = \b -> atomically $ do
                    checkNotTerminated
                    checkNotClosed
                    writeTBQueue tbq $ StreamingBuilder b NotEndOfStream
                , outBodyPushFinal = \b -> atomically $ do
                    checkNotTerminated
                    checkNotClosed
                    writeTVar terminated (Just StreamPushedFinal)
                    writeTBQueue tbq $ StreamingBuilder b (EndOfStream Nothing)
                    writeTBQueue tbq $ StreamingFinished Nothing
                , outBodyFlush = atomically $ do
                    checkNotTerminated
                    checkNotClosed
                    writeTBQueue tbq StreamingFlush
                , outBodyCancel = \mErr -> atomically $ do
                    mTerminated <- readTVar terminated
                    mClosed <- getIsClosed
                    case (mClosed, mTerminated) of
                        (Nothing, Nothing) -> do
                            writeTVar terminated (Just StreamCancelled)
                            writeTBQueue tbq $ StreamingCancelled mErr
                        (Nothing, Just StreamCancelled) ->
                            -- Already cancelled
                            return ()
                        (Nothing, Just _) -> do
                            -- We finished streaming (that is, sending messages to the peer),
                            -- but we must still be able to cancel the stream entirely
                            -- (that is, tell the peer that we no longer want to /receive/ messages: RST_STREAM)
                            writeTVar terminated (Just StreamCancelled)
                            cancelAfterFinish mErr
                        (Just _code, _) ->
                            -- Peer already closed
                            return ()
                }

        finished :: IO ()
        finished = atomically $ do
            mTerminated <- readTVar terminated
            mClosed <- getIsClosed
            case (mClosed, mTerminated) of
                (Nothing, Nothing) -> do
                    writeTVar terminated (Just StreamOutOfScope)
                    writeTBQueue tbq $ StreamingFinished Nothing
                (Nothing, Just _) ->
                    -- We already terminated
                    return ()
                (Just _code, _) ->
                    -- Peer already closed
                    return ()

    k iface `finally` finished