packages feed

quic-simple-0.1.1.0: src/Network/QUIC/Simple/Stream.hs

module Network.QUIC.Simple.Stream
  ( MessageQueues
  , streamSerialise
  , streamCodec
  ) where

import Codec.Serialise (Serialise, serialise, deserialiseIncremental)
import Codec.Serialise qualified as IDecode (IDecode(..))
import Control.Concurrent.Async (Async, async, race_)
import Control.Concurrent.STM
import Control.Exception (finally, throwIO)
import Control.Monad.ST (stToIO)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BSL
import Data.IORef
import Network.QUIC qualified as QUIC

{- | A pair of bounded queues wrapping a stream.
-}
type MessageQueues sendMsg recvMsg = (TBQueue sendMsg, TBQueue recvMsg)

{- | Wrap the stream with the CBOR codec for both incoming and outgoing messages.

The decoder will perform incremental parsing and emit complete messages.

No extra framing is required since CBOR is self-delimiting.
-}
streamSerialise
  :: forall sendMsg recvMsg
  . (Serialise sendMsg, Serialise recvMsg)
  => QUIC.Stream
  -> IO (Async (), MessageQueues sendMsg recvMsg)
streamSerialise stream = do
  initial <- stToIO $ deserialiseIncremental @recvMsg
  state <- newIORef initial
  let
    decode starting chunk = do
      decoder <- readIORef state
      case decoder of
        IDecode.Fail _leftovers _offset err ->
          throwIO err -- crash writer (thus the stream, and the reader/writer etc)
        IDecode.Done leftovers _consumed msg -> do
          stToIO deserialiseIncremental >>= writeIORef state -- restart decoder
          pure (leftovers, Just msg)
        IDecode.Partial consume -> do
          -- want more data (initial state?)
          stToIO (consume $ Just chunk) >>= writeIORef state -- step decoder
          if starting then
            -- re-check if done
            decode False ""
          else
            -- suspend and wait for next chunk
            pure ("", Nothing)
  streamCodec serialise (decode True) stream

{- | Wrap the stream with a codec to provide a TBQueue interface to it.

The decoder loop is stateless.
But it runs in IO so you can use external state and terminate the stream by erroring out.
-}
streamCodec
  :: (sendMsg -> BSL.ByteString) -- ^ Encoder for outgoing messages
  -> (BS.ByteString -> IO (BS.ByteString, Maybe recvMsg)) -- ^ Decoder for incomming chunks
  -> QUIC.Stream
  -> IO (Async (), MessageQueues sendMsg recvMsg)
streamCodec encode decode stream = do
  readQ <- newTBQueueIO 1024
  writeQ <- newTBQueueIO 1024
  worker <- async $
    race_ (reader "" readQ) (writer writeQ) `finally` QUIC.closeStream stream
  pure (worker, (writeQ, readQ))
  where
    reader leftovers readQ = do
      chunk <-
        if BS.null leftovers then
          QUIC.recvStream stream 4096
        else
          pure leftovers
      (leftovers', message_) <- decode chunk
      mapM_ (atomically . writeTBQueue readQ) message_
      reader leftovers' readQ

    writer writeQ = do
      message <- atomically $ readTBQueue writeQ
      let chunks = BSL.toChunks $ encode message
      QUIC.sendStreamMany stream chunks
      writer writeQ