packages feed

prodapi-core-0.1.0.0: src/Prod/Stepper.hs

{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveGeneric #-}

-- | A simplistic stepper for delayable singly-threaded state machines.
module Prod.Stepper where

import qualified Control.Concurrent.Thread.Delay as ThreadDelay
import Control.Monad (when)
import Data.Aeson (FromJSON, ToJSON)
import Data.Coerce (coerce)
import Data.Int (Int64)
import Data.Time.Clock (UTCTime, diffUTCTime, getCurrentTime, nominalDiffTimeToSeconds)
import Data.UnixTime
import Foreign.C.Types (CTime (..))
import GHC.Generics (Generic)

import Prod.Tracer (Tracer (..), runTracer)

-------------------------------------------------------------------------------
data DelaySpec
    = DelaySafetyAmount
    | DelayUntil UTCTime
    | DelayUntilEpochInteger Int64
    deriving (Show, Eq, Ord, Generic)

instance ToJSON DelaySpec
instance FromJSON DelaySpec

type SafetySeconds = Int64

waitUntil :: SafetySeconds -> DelaySpec -> IO ()
waitUntil _ (DelayUntilEpochInteger time) = do
    now <- getUnixTime
    let diff = UnixTime (coerce time) 0 `diffUnixTime` now
    let delayV = udtSeconds diff + 10
    when (delayV > 0) $
        ThreadDelay.delay (toInteger (coerce $ delayV * 1000000 :: Int64))
waitUntil _ (DelayUntil time) = do
    now <- getCurrentTime
    let diff = time `diffUTCTime` now
    let delayV = round (nominalDiffTimeToSeconds diff + 10) :: Int64
    when (delayV > 0) $
        ThreadDelay.delay (toInteger (coerce $ delayV * 1000000 :: Int64))
waitUntil amount DelaySafetyAmount =
    when (amount > 0) $
        ThreadDelay.delay (toInteger (coerce $ amount * 1000000 :: Int64))

isExpired :: DelaySpec -> IO Bool
isExpired (DelayUntilEpochInteger time) = do
    now <- getUnixTime
    let diff = UnixTime (coerce time) 0 `diffUnixTime` now
    let delayV = udtSeconds diff + 10
    pure (delayV <= 0)
isExpired (DelayUntil time) = do
    now <- getCurrentTime
    let diff = time `diffUTCTime` now
    let delayV = round (nominalDiffTimeToSeconds diff + 10) :: Int64
    pure (delayV <= 0)
isExpired DelaySafetyAmount =
    pure False

-------------------------------------------------------------------------------
data Trace input output
    = Start input
    | Suspend DelaySpec input
    | Finish input
    | Delaying DelaySpec input output
    | Inlining input output
    deriving (Show, Eq, Ord, Generic)

instance (ToJSON i, ToJSON o) => ToJSON (Trace i o)
instance (FromJSON i, FromJSON o) => FromJSON (Trace i o)

-------------------------------------------------------------------------------
type BaseStepIO a b = (b -> IO ()) -> a -> IO ()

mapInput :: (c -> a) -> BaseStepIO a b -> BaseStepIO c b
mapInput f g = \consumeA cVal -> g consumeA (f cVal)

mapOutput :: (b -> z) -> BaseStepIO a b -> BaseStepIO a z
mapOutput f g = \consumeZ bVal -> g (consumeZ . f) bVal

-------------------------------------------------------------------------------
data Delayable a = Inline a | Delay DelaySpec a
    deriving (Show, Functor)

-- | Turn a pair of next function into a handler of Delayable.
nextStep :: (DelaySpec -> t1 -> t2) -> (t1 -> t2) -> Delayable t1 -> t2
nextStep _ inlineF (Inline val) = inlineF val
nextStep delayF _ (Delay d val) = delayF d val

-------------------------------------------------------------------------------
type StepIO a b = BaseStepIO (Delayable a) (Delayable b)

-- special case for a StepIO that will start immediately (removing the need to handle the Delayable branch)
type StartStepIO a b = BaseStepIO a (Delayable b)

-------------------------------------------------------------------------------
data ExecFunctions arg = ExecFunctions
    { inline :: arg -> IO ()
    , delay :: DelaySpec -> arg -> IO ()
    }

defineExecution ::
    -- | tracer to save the state
    Tracer IO (Trace z1 z2) ->
    -- | function to turn an input into a state we hope to resume
    (a -> z1) ->
    (b -> z2) ->
    -- | handler-defining function with a bag of delay/inline functions and a start value
    (ExecFunctions b -> a -> IO ()) ->
    StepIO a b
defineExecution tracer f g runStep = \next input -> do
    -- callback given to downstream consumer: so that we can delay
    let delayF val delaySpec nextObj = do
            let mappedOutput = g nextObj
            runTracer tracer (Delaying delaySpec val mappedOutput)
            next (Delay delaySpec nextObj)

    -- callback given to downstream consumer: so that we can inline
    let inlineF val nextObj = do
            let mappedOutput = g nextObj
            runTracer tracer (Inlining val mappedOutput)
            next (Inline nextObj)

    -- hanling of input
    case input of
        Inline val -> do
            let mappedInput = f val
            runTracer tracer (Start mappedInput)
            runStep (ExecFunctions (inlineF mappedInput) (delayF mappedInput)) val
            runTracer tracer (Finish mappedInput)
        Delay delaySpec val -> do
            let mappedInput = f val
            runTracer tracer (Suspend delaySpec mappedInput)

onSuspend :: (DelaySpec -> a -> IO ()) -> Tracer IO (Trace a b)
onSuspend action =
    Tracer go
  where
    go (Suspend delaySpec obj) = do
        action delaySpec obj
    go _ = do
        pure ()