packages feed

extensible-effects-concurrent-2.0.0: src/Control/Eff/Concurrent/Protocol/EffectfulServer.hs

-- | Utilities to implement /effectful server-loops/.
--
-- @since 0.24.0
module Control.Eff.Concurrent.Protocol.EffectfulServer
  ( Server (..),
    Event (..),
    start,
    startLink,
    protocolServerLoop,
  )
where

import Control.Applicative
import Control.DeepSeq
import Control.Eff
import Control.Eff.Concurrent.Process
import Control.Eff.Concurrent.Process.Timer
import Control.Eff.Concurrent.Protocol
import Control.Eff.Concurrent.Protocol.Wrapper
import Control.Eff.Extend ()
import Control.Eff.Log
import Control.Lens
import Data.Kind
import Data.Typeable

-- | A type class for effectful server loops.
--
-- This type class serves as interface for other abstractions, for example /process supervision/
--
-- The methods of this class handle 'Event's 'Request's for 'Pdu' instance.
--
-- Instances can by /index types/ for 'Pdu' family directly, or indirectly via the 'ServerPdu' type family.
--
-- To builder servers serving multiple protocols, use the generic 'Pdu' instances, for which 'Embeds'
-- instances exist, like 2-,3-,4-, or 5-tuple.
--
-- @since 0.24.1
class (ToLogMsg (Init a)) => Server (a :: Type) (e :: [Type -> Type]) where
  -- | The value that defines what is required to initiate a 'Server'
  -- loop.
  data Init a

  -- | The index type of the 'Event's that this server processes.
  -- This is the first parameter to the 'Request' and therefore of
  -- the 'Pdu' family.
  type ServerPdu a :: Type -- TODO get rid of ...

  type ServerPdu a = a

  -- | Effects of the implementation
  --
  -- @since 0.24.1
  type ServerEffects a e :: [Type -> Type]

  type ServerEffects a e = e

  -- | Return the 'ProcessTitle'.
  --
  -- Usually you should rely on the default implementation
  serverTitle :: Init a -> ProcessTitle
  default serverTitle :: ToTypeLogMsg a => Init a -> ProcessTitle
  serverTitle x = MkProcessTitle (toTypeLogMsg x)

  -- | Process the effects of the implementation
  runEffects :: Endpoint (ServerPdu a) -> Init a -> Eff (ServerEffects a e) x -> Eff e x
  default runEffects :: ServerEffects a e ~ e => Endpoint (ServerPdu a) -> Init a -> Eff (ServerEffects a e) x -> Eff e x
  runEffects _ = const id

  -- | Update the 'Model' based on the 'Event'.
  onEvent :: Endpoint (ServerPdu a) -> Init a -> Event (ServerPdu a) -> Eff (ServerEffects a e) ()
  default onEvent :: (Member Logs (ServerEffects a e)) => Endpoint (ServerPdu a) -> Init a -> Event (ServerPdu a) -> Eff (ServerEffects a e) ()
  onEvent _ i e = logInfo (MkUnhandledEvent i e)

data UnhandledEvent a where
  MkUnhandledEvent ::
    ( ToLogMsg (Init a),
      ToLogMsg (Event (ServerPdu a))
    ) =>
    Init a ->
    Event (ServerPdu a) ->
    UnhandledEvent a

instance ToLogMsg (UnhandledEvent a) where
  toLogMsg (MkUnhandledEvent i e) = packLogMsg "unhandled event: " <> toLogMsg e <> packLogMsg " init: " <> toLogMsg i

-- | Execute the server loop.
--
-- @since 0.24.0
start ::
  forall a r q.
  ( Server a (Processes q),
    Typeable (ServerPdu a),
    FilteredLogging (Processes q),
    HasProcesses (ServerEffects a (Processes q)) q,
    HasProcesses r q
  ) =>
  Init a ->
  Eff r (Endpoint (ServerPdu a))
start a = asEndpoint <$> spawn (serverTitle @_ @(Processes q) a) (protocolServerLoop a)

-- | Execute the server loop.
--
-- @since 0.24.0
startLink ::
  forall a r q.
  ( Typeable (ServerPdu a),
    Server a (Processes q),
    FilteredLogging (Processes q),
    HasProcesses (ServerEffects a (Processes q)) q,
    HasProcesses r q
  ) =>
  Init a ->
  Eff r (Endpoint (ServerPdu a))
startLink a = asEndpoint <$> spawnLink (serverTitle @_ @(Processes q) a) (protocolServerLoop a)

-- | Execute the server loop.
--
-- @since 0.24.0
protocolServerLoop ::
  forall q a.
  ( Server a (Processes q),
    FilteredLogging (Processes q),
    HasProcesses (ServerEffects a (Processes q)) q,
    Typeable (ServerPdu a)
  ) =>
  Init a ->
  Eff (Processes q) ()
protocolServerLoop a = do
  myEp <- asEndpoint @(ServerPdu a) <$> self
  logDebug ("starting" :: String)
  runEffects myEp a (receiveSelectedLoop sel (mainLoop myEp))
  return ()
  where
    sel :: MessageSelector (Event (ServerPdu a))
    sel =
      onRequest <$> selectMessage @(Request (ServerPdu a))
        <|> OnDown <$> selectMessage @ProcessDown
        <|> OnTimeOut <$> selectMessage @TimerElapsed
        <|> OnMessage <$> selectAnyMessage
      where
        onRequest :: Request (ServerPdu a) -> Event (ServerPdu a)
        onRequest (Call o m) = OnCall (replyTarget (MkSerializer toMessage) o) m
        onRequest (Cast m) = OnCast m
    handleInt myEp i = onEvent @_ @(Processes q) myEp a (OnInterrupt i) *> pure Nothing
    mainLoop ::
      Endpoint (ServerPdu a) ->
      Either InterruptReason (Event (ServerPdu a)) ->
      Eff (ServerEffects a (Processes q)) (Maybe ())
    mainLoop myEp (Left i) = handleInt myEp i
    mainLoop myEp (Right i) = onEvent @_ @(Processes q) myEp a i *> pure Nothing

-- | This event sum-type is used to communicate incoming messages and other events to the
-- instances of 'Server'.
--
-- @since 0.24.0
data Event a where
  -- | A 'Synchronous' message was received. If an implementation wants to delegate nested 'Pdu's, it can
  -- use 'toEmbeddedReplyTarget' to convert a 'ReplyTarget' safely to the embedded protocol.
  --
  -- @since 0.24.1
  OnCall ::
    forall a r.
    ( Tangible r,
      TangiblePdu a ('Synchronous r),
      ToLogMsg (Pdu a ('Synchronous r))
    ) =>
    ReplyTarget a r ->
    Pdu a ('Synchronous r) ->
    Event a
  OnCast ::
    forall a.
    ( TangiblePdu a 'Asynchronous,
      ToLogMsg (Pdu a 'Asynchronous)
    ) =>
    Pdu a 'Asynchronous ->
    Event a
  OnInterrupt :: InterruptReason -> Event a
  OnDown :: ProcessDown -> Event a
  OnTimeOut :: TimerElapsed -> Event a
  OnMessage :: Message -> Event a
  deriving (Typeable)

instance ToLogMsg (Event a) where
  toLogMsg x =
    packLogMsg "event: "
      <> case x of
        OnCall o p -> toLogMsg (Call (view replyTargetOrigin o) p)
        OnCast p -> toLogMsg (Cast p)
        OnInterrupt r -> toLogMsg r
        OnDown r -> toLogMsg r
        OnTimeOut r -> toLogMsg r
        OnMessage r -> packLogMsg "message: " <> packLogMsg (show r)

instance NFData (Event a) where
  rnf = \case
    OnCall o p -> rnf o `seq` rnf p
    OnCast p -> rnf p
    OnInterrupt r -> rnf r
    OnDown r -> rnf r
    OnTimeOut r -> rnf r
    OnMessage r -> r `seq` ()