packages feed

tigerbeetle-hs-0.1.0.0: src/Database/TigerBeetle/Raw/Client.hs

{-# LANGUAGE LambdaCase #-}

module Database.TigerBeetle.Raw.Client
  ( -- * Types
    ClientInitError (..)
  , ClientPtr

    -- * Functions
  , initClient
  , submit
  , FFI.makeCompletionCallback
  )
where

import Control.Exception (assert)
import Control.Monad.IO.Class
import Data.ByteString (ByteString)
import Data.ByteString qualified as BS
import Data.Text.Encoding qualified as TE
import Database.TigerBeetle.Address
import Database.TigerBeetle.ClusterId (ClusterId)
import Database.TigerBeetle.Internal.FFI.Client
  ( TBClientStatus (..)
  , TBCompletionCallback
  , TBCompletionContext
  , TBInitStatus
  , TBOperation (..)
  , TBPacket (..)
  , TBPacketStatus (..)
  )
import Database.TigerBeetle.Internal.FFI.Client qualified as FFI
import Database.TigerBeetle.Raw.Response (DecodeResponseError)
import Foreign.C.Types (CChar)
import Foreign.ForeignPtr (ForeignPtr, mallocForeignPtr, withForeignPtr)
import Foreign.Ptr (FunPtr, Ptr)

-- | Whether to start an echo server or a standard server
data ClientKind = Echo | Standard
  deriving (Eq, Ord, Show)

data RequestError
  = ClientError TBClientStatus
  | PacketError TBPacketStatus
  | PacketDataParseError DecodeResponseError
  | ClientShutdownDuringRequest
  | RequestTimeoutError TBOperation ByteString
  deriving (Eq, Show)

withAddressPtr :: Address -> ((Ptr CChar, Int) -> IO a) -> IO a
withAddressPtr = BS.useAsCStringLen . TE.encodeUtf8 . getAddress

data ClientInitError
  = Unexpected
  | OutOfMemory
  | AddressInvalid
  | AddressLimitExceeded
  | SystemResources
  | NetworkSubsystem
  deriving (Eq, Show)

-- | Create a 'Client' initialization error from a 'TBInitStatus'.
--
-- Asserts that @err@ is not 'FFI.Success', throws an exception at
-- runtime.
toClientInitError :: TBInitStatus -> ClientInitError
toClientInitError err = assert (err /= FFI.Success) $
  case err of
    FFI.Unexpected -> Unexpected
    FFI.OutOfMemory -> OutOfMemory
    FFI.AddressInvalid -> AddressInvalid
    FFI.AddressLimitExceeded -> AddressLimitExceeded
    FFI.SystemResources -> SystemResources
    FFI.NetworkSubsystem -> NetworkSubsystem
    FFI.Success -> error "toClientInitError: Success is not an error"

type ClientPtr = ForeignPtr FFI.TBClient

initClientPtr :: IO ClientPtr
initClientPtr = do
  mallocForeignPtr

validateClientInit :: ClientPtr -> TBInitStatus -> IO (Either ClientInitError ClientPtr)
validateClientInit clientPtr = \case
  FFI.Success -> pure $ Right clientPtr
  initError -> pure . Left . toClientInitError $ initError

-- | Call @tb_client_init@ and return a valid 'Client' upon success.
--
-- The finalizer on 'Client' will call @tb_client_deinit@.
initClient
  :: ClusterId
  -> Address
  -> TBCompletionContext
  -> FunPtr TBCompletionCallback
  -> IO (Either ClientInitError ClientPtr)
initClient clusterId address completionCtx completionCallback = do
  clientPtr <- initClientPtr
  initStatus <- withForeignPtr clientPtr $ \cp -> do
    withAddressPtr address $ \(addrPtr, addrLen) -> do
      FFI.tbClientInit
        cp
        clusterId
        addrPtr
        (fromIntegral addrLen)
        completionCtx
        completionCallback
  validateClientInit clientPtr initStatus

-- | Call @tb_client_submit@
submit :: (MonadIO m) => ClientPtr -> (a -> IO (ForeignPtr TBPacket)) -> a -> m TBClientStatus
submit clientPtr action param = do
  requestPacketPtr <- liftIO $ action param
  liftIO $ withForeignPtr clientPtr $ \rawClient -> do
    withForeignPtr requestPacketPtr $ \rawPacket -> do
      FFI.tbClientSubmit rawClient rawPacket