packages feed

gbnet-hs-0.2.2.0: src/GBNet/Net.hs

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

-- |
-- Module      : GBNet.Net
-- Description : NetT monad transformer for network operations
--
-- Provides a monad transformer that carries network state (socket, local address)
-- and implements MonadNetwork. This allows polymorphic network code that can
-- run against real IO or pure test backends.
--
-- The IO backend uses a dedicated receive thread that blocks efficiently on
-- the socket via GHC's IO manager (epoll\/kqueue), delivering packets through
-- an STM 'TQueue' with zero polling overhead.
module GBNet.Net
  ( -- * Network state
    NetState (..),
    newNetState,

    -- * NetT monad transformer
    NetT (..),
    runNetT,
    evalNetT,
    execNetT,

    -- * State access
    getNetState,
    getsNetState,
    modifyNetState,
  )
where

import Control.Concurrent (ThreadId, forkIO, killThread)
import Control.Concurrent.STM (TQueue, atomically, newTQueueIO, tryReadTQueue, writeTQueue)
import Control.Exception (IOException, handle)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.State.Strict (StateT (..), evalStateT, execStateT, get, gets, modify')
import qualified Data.ByteString as BS
import GBNet.Class (MonadNetwork (..), MonadTime (..), NetError (..), getMonoTimeIO)
import GBNet.Security (validateAndStripCrc32)
import GBNet.Socket
  ( UdpSocket (..),
    closeSocket,
    maxUdpPacketSize,
    socketSendTo,
  )
import Network.Socket (SockAddr, Socket)
import qualified Network.Socket.ByteString as NSB
import Optics ((%), (%~), (&), (.~), (?~))
import Optics.TH (makeFieldLabelsNoPrefix)

-- | Network state carried by NetT.
--
-- Contains a 'TQueue' fed by a dedicated receive thread, eliminating
-- the need for polling timeouts on the socket.
data NetState = NetState
  { -- | The UDP socket (used for sending)
    nsSocket :: !UdpSocket,
    -- | Local address this peer is bound to
    nsLocalAddr :: !SockAddr,
    -- | Queue of received packets from the receive thread
    nsRecvQueue :: !(TQueue (BS.ByteString, SockAddr)),
    -- | Receive thread handle (for cleanup)
    nsRecvThread :: !ThreadId
  }

makeFieldLabelsNoPrefix ''NetState

-- | Create network state and start the dedicated receive thread.
--
-- The receive thread blocks on the socket via GHC's IO manager
-- (epoll on Linux, kqueue on macOS), consuming zero CPU when idle.
-- Received packets are delivered through an STM 'TQueue'.
newNetState :: UdpSocket -> SockAddr -> IO NetState
newNetState sock addr = do
  queue <- newTQueueIO
  tid <- forkIO $ recvLoop (usSocket sock) queue
  pure
    NetState
      { nsSocket = sock,
        nsLocalAddr = addr,
        nsRecvQueue = queue,
        nsRecvThread = tid
      }

-- | Dedicated receive thread.
--
-- Blocks on @recvFrom@ which GHC's runtime handles efficiently via the
-- IO manager — the green thread is parked with no CPU cost until data
-- arrives. Exits cleanly when the socket is closed.
recvLoop :: Socket -> TQueue (BS.ByteString, SockAddr) -> IO ()
recvLoop sock queue = go
  where
    go =
      handle (\(_ :: IOException) -> pure ()) $ do
        (dat, addr) <- NSB.recvFrom sock maxUdpPacketSize
        atomically $ writeTQueue queue (dat, addr)
        go

-- | Network monad transformer.
--
-- Wraps StateT to carry network state. When @m@ is IO, this provides real
-- UDP networking via the 'MonadNetwork' instance defined below.
-- For testing, use @TestNet@ from "GBNet.TestNet" instead.
newtype NetT m a = NetT {unNetT :: StateT NetState m a}
  deriving (Functor, Applicative, Monad)

-- | Run a NetT computation with initial state, returning result and final state.
runNetT :: NetT m a -> NetState -> m (a, NetState)
runNetT (NetT m) = runStateT m

-- | Run a NetT computation, discarding final state.
evalNetT :: (Monad m) => NetT m a -> NetState -> m a
evalNetT (NetT m) = evalStateT m

-- | Run a NetT computation, returning only final state.
execNetT :: (Monad m) => NetT m a -> NetState -> m NetState
execNetT (NetT m) = execStateT m

-- | Get the current network state.
getNetState :: (Monad m) => NetT m NetState
getNetState = NetT get

-- | Get a field from network state.
getsNetState :: (Monad m) => (NetState -> a) -> NetT m a
getsNetState = NetT . gets

-- | Modify network state.
modifyNetState :: (Monad m) => (NetState -> NetState) -> NetT m ()
modifyNetState = NetT . modify'

-- | MonadIO instance for NetT.
instance (MonadIO m) => MonadIO (NetT m) where
  liftIO = NetT . liftIO

-- | MonadTime instance for NetT IO.
instance MonadTime (NetT IO) where
  getMonoTime = liftIO getMonoTimeIO

-- | MonadNetwork instance for NetT IO.
--
-- Receives are non-blocking reads from the TQueue (fed by the dedicated
-- receive thread). Sends go directly through the socket.
instance MonadNetwork (NetT IO) where
  netSend toAddr bytes = do
    st <- getNetState
    now <- liftIO getMonoTimeIO
    result <- liftIO $ socketSendTo bytes toAddr now (nsSocket st)
    case result of
      Left err -> pure $ Left (NetSendFailed (show err))
      Right (_, updatedSock) -> do
        modifyNetState $ #nsSocket .~ updatedSock
        pure $ Right ()

  netRecv = do
    st <- getNetState
    result <- liftIO $ atomically $ tryReadTQueue (nsRecvQueue st)
    case result of
      Nothing -> pure Nothing
      Just (dat, addr) -> do
        -- Update receive stats on the socket
        now <- liftIO getMonoTimeIO
        let len = BS.length dat
        modifyNetState $
          #nsSocket
            % #usStats
            %~ ( \stats ->
                   stats
                     & #ssBytesReceived
                     %~ (+ fromIntegral len)
                     & #ssPacketsReceived
                     %~ (+ 1)
                     & #ssLastReceiveTime
                     ?~ now
               )
        -- Validate CRC before returning
        case validateAndStripCrc32 dat of
          Nothing -> do
            -- Increment CRC drop counter
            modifyNetState $ #nsSocket % #usStats % #ssCrcDrops %~ (+ 1)
            pure Nothing
          Just validated -> pure $ Just (validated, addr)

  netClose = do
    st <- getNetState
    liftIO $ killThread (nsRecvThread st)
    liftIO $ closeSocket (nsSocket st)