packages feed

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

module Network.QUIC.Simple.Stream
  ( -- $intro

    -- * Message queues
    MessageQueues
  , serialise
  , serialiseFrom
  , codec
  , codecFrom
    -- * One-shot messages
  , sendMessage
  , recvMessage
    -- * Raw bytes
  , consume
    -- * Multiple streams
  , open
  , acceptLoop
  ) where

import Codec.Serialise (Serialise, deserialiseIncremental)
import Codec.Serialise qualified as CBOR
import Codec.Serialise qualified as IDecode (IDecode(..))
import Control.Concurrent.Async (Async, async, cancel, link, poll, race_)
import Control.Concurrent.STM
import Control.Exception (finally, throwIO)
import Control.Monad (filterM, forever, unless)
import Control.Monad.ST (stToIO)
import Data.ByteString qualified as BS
import Data.ByteString.Lazy qualified as BSL
import Data.IORef
import Data.Maybe (isNothing)
import Network.QUIC qualified as QUIC

{- $intro
This module is meant to be imported qualified:

> import Network.QUIC.Simple.Stream qualified as Stream

The connection wrappers in "Network.QUIC.Simple" hand out the first stream.
The functions here run protocols over it, or open and accept more streams.
-}

{- | 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.
-}
serialise
  :: (Serialise sendMsg, Serialise recvMsg)
  => QUIC.Stream
  -> IO (Async (), MessageQueues sendMsg recvMsg)
serialise = serialiseFrom ""

{- | Same as 'serialise', but starts decoding from the bytes already received.

Use with the leftovers from 'recvMessage' when a stream starts with a header.
-}
serialiseFrom
  :: forall sendMsg recvMsg
  . (Serialise sendMsg, Serialise recvMsg)
  => BS.ByteString
  -> QUIC.Stream
  -> IO (Async (), MessageQueues sendMsg recvMsg)
serialiseFrom leftovers 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)
  codecFrom CBOR.serialise (decode True) leftovers 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.

The worker finishes when the peer closes the stream, or when either the codec or the stream fails.
Cancel it to close the stream from this side.
Messages still queued for sending at that moment are dropped.
-}
codec
  :: (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)
codec encode decode = codecFrom encode decode ""

{- | Same as 'codec', but starts decoding from the bytes already received.
-}
codecFrom
  :: (sendMsg -> BSL.ByteString) -- ^ Encoder for outgoing messages
  -> (BS.ByteString -> IO (BS.ByteString, Maybe recvMsg)) -- ^ Decoder for incomming chunks
  -> BS.ByteString -- ^ Bytes received before the codec took over
  -> QUIC.Stream
  -> IO (Async (), MessageQueues sendMsg recvMsg)
codecFrom encode decode leftovers stream = do
  readQ <- newTBQueueIO 1024
  writeQ <- newTBQueueIO 1024
  worker <- async $
    race_ (reader leftovers readQ) (writer writeQ) `finally` QUIC.closeStream stream
  pure (worker, (writeQ, readQ))
  where
    reader buffered readQ = do
      chunk <-
        if BS.null buffered then
          QUIC.recvStream stream 4096
        else
          pure buffered
      unless (BS.null chunk) do
        (buffered', message_) <- decode chunk
        mapM_ (atomically . writeTBQueue readQ) message_
        reader buffered' readQ

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

{- | Send a single CBOR message to a stream.
-}
sendMessage :: Serialise msg => QUIC.Stream -> msg -> IO ()
sendMessage stream = QUIC.sendStreamMany stream . BSL.toChunks . CBOR.serialise

{- | Receive a single CBOR message from a stream.

Bytes received past the end of the message are returned along with it.
They belong to whatever comes next on the stream,
so feed them to 'serialiseFrom' or 'consume'.
In a lockstep exchange there will be none.

Throws a decoding error if the stream ends or gets garbled before the message is complete.
-}
recvMessage :: forall msg. Serialise msg => QUIC.Stream -> IO (msg, BS.ByteString)
recvMessage stream = stToIO (deserialiseIncremental @msg) >>= step
  where
    step = \case
      IDecode.Done leftovers _offset msg ->
        pure (msg, leftovers)
      IDecode.Fail _leftovers _offset err ->
        throwIO err
      IDecode.Partial consume_ -> do
        chunk <- QUIC.recvStream stream 4096
        stToIO (consume_ $ if BS.null chunk then Nothing else Just chunk) >>= step

{- | Feed the incoming bytes to a sink until the peer closes the stream.

The bytes received before, e.g. the leftovers from 'recvMessage', go first.
-}
consume :: BS.ByteString -> QUIC.Stream -> (BS.ByteString -> IO ()) -> IO ()
consume leftovers stream sink = do
  unless (BS.null leftovers) $ sink leftovers
  drain
  where
    drain = do
      chunk <- QUIC.recvStream stream 65536
      unless (BS.null chunk) do
        sink chunk
        drain

{- | Request a new bidirectional stream and announce its purpose with a header message.

The peer is expected to run 'acceptLoop' with a matching header type.
Anything sent right after the header will arrive as leftovers on the other side.
-}
open :: Serialise header => QUIC.Connection -> header -> IO QUIC.Stream
open conn header = do
  new <- QUIC.stream conn
  sendMessage new header
  pure new

{- | Accept streams opened by the peer and dispatch them by their header message.

Each stream gets a thread of its own that closes the stream when the handler returns.
The handler receives the bytes that arrived along with the header,
to be passed along to 'serialiseFrom' or 'consume'.

A crashing handler takes down the accept loop, and thus the connection.
Leaving the loop, by cancelling it or by connection failure, cancels the handlers still running.
-}
acceptLoop
  :: Serialise header
  => QUIC.Connection
  -> (header -> BS.ByteString -> QUIC.Stream -> IO ())
  -> IO ()
acceptLoop conn handler = do
  running <- newTVarIO []
  let
    cancelAll = readTVarIO running >>= mapM_ cancel
    prune = readTVarIO running >>= filterM (fmap isNothing . poll) >>= atomically . writeTVar running
    handleStream new = do
      (header, leftovers) <- recvMessage new
      handler header leftovers new
    accept = forever do
      new <- QUIC.acceptStream conn
      worker <- async $ handleStream new `finally` QUIC.closeStream new
      link worker
      prune
      atomically $ modifyTVar' running (worker :)
  accept `finally` cancelAll