module Ecluse.Core.Credential.Refresh.Internal (
RefreshConfig (..),
defaultRefreshConfig,
refreshingProvider,
refreshingProviderWith,
RefreshReporter (..),
noRefreshReporter,
CredentialReporters (..),
noCredentialReporters,
CredentialError (..),
CacheState (..),
ServeAction (..),
decide,
refreshDueAt,
onMintSuccess,
onMintFailure,
admitMint,
releaseSingleFlight,
) where
import Control.Concurrent.STM (retry)
import Data.Time (NominalDiffTime, UTCTime, addUTCTime, diffUTCTime)
import UnliftIO (asyncWithUnmask, throwIO, try)
import UnliftIO.Exception (mask)
import Ecluse.Core.Breaker (
Breaker,
BreakerReporter,
admit,
initialBreaker,
noBreakerReporter,
recordFailure,
recordSuccess,
reportBreakerChange,
)
import Ecluse.Core.Credential (AuthToken (..), CredentialProvider (..))
import Ecluse.Core.InFlight (guardInFlight)
data CredentialError
=
BreakerOpen
|
Unconfigured Text
|
MintedTokenAlreadyExpired
deriving stock (CredentialError -> CredentialError -> Bool
(CredentialError -> CredentialError -> Bool)
-> (CredentialError -> CredentialError -> Bool)
-> Eq CredentialError
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: CredentialError -> CredentialError -> Bool
== :: CredentialError -> CredentialError -> Bool
$c/= :: CredentialError -> CredentialError -> Bool
/= :: CredentialError -> CredentialError -> Bool
Eq, Int -> CredentialError -> ShowS
[CredentialError] -> ShowS
CredentialError -> String
(Int -> CredentialError -> ShowS)
-> (CredentialError -> String)
-> ([CredentialError] -> ShowS)
-> Show CredentialError
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> CredentialError -> ShowS
showsPrec :: Int -> CredentialError -> ShowS
$cshow :: CredentialError -> String
show :: CredentialError -> String
$cshowList :: [CredentialError] -> ShowS
showList :: [CredentialError] -> ShowS
Show)
instance Exception CredentialError
data RefreshReporter = RefreshReporter
{ RefreshReporter -> Maybe Int -> IO ()
onRefreshSucceeded :: Maybe Int -> IO ()
, RefreshReporter -> Maybe Int -> IO ()
onRefreshFailed :: Maybe Int -> IO ()
}
noRefreshReporter :: RefreshReporter
noRefreshReporter :: RefreshReporter
noRefreshReporter = (Maybe Int -> IO ()) -> (Maybe Int -> IO ()) -> RefreshReporter
RefreshReporter (IO () -> Maybe Int -> IO ()
forall a b. a -> b -> a
const IO ()
forall (f :: * -> *). Applicative f => f ()
pass) (IO () -> Maybe Int -> IO ()
forall a b. a -> b -> a
const IO ()
forall (f :: * -> *). Applicative f => f ()
pass)
data CredentialReporters = CredentialReporters
{ CredentialReporters -> BreakerReporter
crBreakerReporter :: BreakerReporter
, CredentialReporters -> RefreshReporter
crRefreshReporter :: RefreshReporter
}
noCredentialReporters :: CredentialReporters
noCredentialReporters :: CredentialReporters
noCredentialReporters = BreakerReporter -> RefreshReporter -> CredentialReporters
CredentialReporters BreakerReporter
noBreakerReporter RefreshReporter
noRefreshReporter
data RefreshConfig = RefreshConfig
{ RefreshConfig -> IO AuthToken
rcMint :: IO AuthToken
, RefreshConfig -> IO UTCTime
rcClock :: IO UTCTime
, RefreshConfig -> IO Double
rcJitter :: IO Double
, RefreshConfig -> Double
rcRefreshAt :: Double
, RefreshConfig -> NominalDiffTime
rcRefreshFloor :: NominalDiffTime
, RefreshConfig -> Int
rcBreakerThreshold :: Int
, RefreshConfig -> NominalDiffTime
rcBreakerCooldown :: NominalDiffTime
, RefreshConfig -> BreakerReporter
rcBreakerReporter :: BreakerReporter
, RefreshConfig -> RefreshReporter
rcRefreshReporter :: RefreshReporter
}
defaultRefreshConfig :: RefreshConfig
defaultRefreshConfig :: RefreshConfig
defaultRefreshConfig =
RefreshConfig
{ rcMint :: IO AuthToken
rcMint = Text -> IO AuthToken
forall a. Text -> IO a
unconfigured Text
"rcMint"
, rcClock :: IO UTCTime
rcClock = Text -> IO UTCTime
forall a. Text -> IO a
unconfigured Text
"rcClock"
, rcJitter :: IO Double
rcJitter = Double -> IO Double
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Double
0
, rcRefreshAt :: Double
rcRefreshAt = Double
0.8
, rcRefreshFloor :: NominalDiffTime
rcRefreshFloor = NominalDiffTime
30
, rcBreakerThreshold :: Int
rcBreakerThreshold = Int
5
, rcBreakerCooldown :: NominalDiffTime
rcBreakerCooldown = NominalDiffTime
60
, rcBreakerReporter :: BreakerReporter
rcBreakerReporter = BreakerReporter
noBreakerReporter
, rcRefreshReporter :: RefreshReporter
rcRefreshReporter = RefreshReporter
noRefreshReporter
}
where
unconfigured :: Text -> IO a
unconfigured :: forall a. Text -> IO a
unconfigured Text
field = CredentialError -> IO a
forall (m :: * -> *) e a. (MonadIO m, Exception e) => e -> m a
throwIO (Text -> CredentialError
Unconfigured Text
field)
data CacheState = CacheState
{ CacheState -> AuthToken
csToken :: AuthToken
, CacheState -> Maybe UTCTime
csRefreshDue :: Maybe UTCTime
, CacheState -> Bool
csRefreshing :: Bool
, CacheState -> Breaker
csBreaker :: Breaker
}
refreshingProvider :: RefreshConfig -> IO CredentialProvider
refreshingProvider :: RefreshConfig -> IO CredentialProvider
refreshingProvider = IO () -> RefreshConfig -> IO CredentialProvider
refreshingProviderWith (() -> IO ()
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ())
refreshingProviderWith :: IO () -> RefreshConfig -> IO CredentialProvider
refreshingProviderWith :: IO () -> RefreshConfig -> IO CredentialProvider
refreshingProviderWith IO ()
afterClaim RefreshConfig
cfg = do
now <- RefreshConfig -> IO UTCTime
rcClock RefreshConfig
cfg
token <- rcMint cfg
due <- refreshDueAt cfg now token
stateVar <- newTVarIO (CacheState token due False initialBreaker)
pure CredentialProvider{currentToken = serve afterClaim cfg stateVar}
serve :: IO () -> RefreshConfig -> TVar CacheState -> IO AuthToken
serve :: IO () -> RefreshConfig -> TVar CacheState -> IO AuthToken
serve IO ()
afterClaim RefreshConfig
cfg TVar CacheState
stateVar = ((forall a. IO a -> IO a) -> IO AuthToken) -> IO AuthToken
forall (m :: * -> *) b.
MonadUnliftIO m =>
((forall a. m a -> m a) -> m b) -> m b
mask (((forall a. IO a -> IO a) -> IO AuthToken) -> IO AuthToken)
-> ((forall a. IO a -> IO a) -> IO AuthToken) -> IO AuthToken
forall a b. (a -> b) -> a -> b
$ \forall a. IO a -> IO a
restore -> do
now <- RefreshConfig -> IO UTCTime
rcClock RefreshConfig
cfg
action <- atomically (decide stateVar now)
case action of
ServeCached AuthToken
token -> AuthToken -> IO AuthToken
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure AuthToken
token
ServeAndRefresh AuthToken
token -> do
_ <-
((forall a. IO a -> IO a) -> IO ()) -> IO (Async ())
forall (m :: * -> *) a.
MonadUnliftIO m =>
((forall b. m b -> m b) -> m a) -> m (Async a)
asyncWithUnmask (((forall a. IO a -> IO a) -> IO ()) -> IO (Async ()))
-> ((forall a. IO a -> IO a) -> IO ()) -> IO (Async ())
forall a b. (a -> b) -> a -> b
$ \forall a. IO a -> IO a
unmask ->
(IO () -> IO ())
-> (SomeException -> IO ()) -> IO () -> IO () -> IO ()
forall a.
(IO a -> IO a) -> (SomeException -> IO ()) -> IO () -> IO a -> IO a
guardInFlight IO () -> IO ()
forall a. IO a -> IO a
unmask SomeException -> IO ()
noWaiter (TVar CacheState -> IO ()
releaseSingleFlight TVar CacheState
stateVar) (IO ()
afterClaim IO () -> IO () -> IO ()
forall a b. IO a -> IO b -> IO b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> RefreshConfig -> TVar CacheState -> IO ()
backgroundRefresh RefreshConfig
cfg TVar CacheState
stateVar)
pure token
ServeAction
MintNow ->
(IO AuthToken -> IO AuthToken)
-> (SomeException -> IO ())
-> IO ()
-> IO AuthToken
-> IO AuthToken
forall a.
(IO a -> IO a) -> (SomeException -> IO ()) -> IO () -> IO a -> IO a
guardInFlight IO AuthToken -> IO AuthToken
forall a. IO a -> IO a
restore SomeException -> IO ()
noWaiter (TVar CacheState -> IO ()
releaseSingleFlight TVar CacheState
stateVar) (IO ()
afterClaim IO () -> IO AuthToken -> IO AuthToken
forall a b. IO a -> IO b -> IO b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> RefreshConfig -> TVar CacheState -> IO AuthToken
mintSynchronously RefreshConfig
cfg TVar CacheState
stateVar)
where
noWaiter :: SomeException -> IO ()
noWaiter :: SomeException -> IO ()
noWaiter = IO () -> SomeException -> IO ()
forall a b. a -> b -> a
const IO ()
forall (f :: * -> *). Applicative f => f ()
pass
decide :: TVar CacheState -> UTCTime -> STM ServeAction
decide :: TVar CacheState -> UTCTime -> STM ServeAction
decide TVar CacheState
stateVar UTCTime
now = do
st <- TVar CacheState -> STM CacheState
forall a. TVar a -> STM a
readTVar TVar CacheState
stateVar
if tokenValid now (csToken st)
then
if refreshNeeded now st && not (csRefreshing st)
then do
writeTVar stateVar st{csRefreshing = True}
pure (ServeAndRefresh (csToken st))
else pure (ServeCached (csToken st))
else
if csRefreshing st
then retry
else do
writeTVar stateVar st{csRefreshing = True}
pure MintNow
data ServeAction
=
ServeCached AuthToken
|
ServeAndRefresh AuthToken
|
MintNow
deriving stock (ServeAction -> ServeAction -> Bool
(ServeAction -> ServeAction -> Bool)
-> (ServeAction -> ServeAction -> Bool) -> Eq ServeAction
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ServeAction -> ServeAction -> Bool
== :: ServeAction -> ServeAction -> Bool
$c/= :: ServeAction -> ServeAction -> Bool
/= :: ServeAction -> ServeAction -> Bool
Eq, Int -> ServeAction -> ShowS
[ServeAction] -> ShowS
ServeAction -> String
(Int -> ServeAction -> ShowS)
-> (ServeAction -> String)
-> ([ServeAction] -> ShowS)
-> Show ServeAction
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ServeAction -> ShowS
showsPrec :: Int -> ServeAction -> ShowS
$cshow :: ServeAction -> String
show :: ServeAction -> String
$cshowList :: [ServeAction] -> ShowS
showList :: [ServeAction] -> ShowS
Show)
backgroundRefresh :: RefreshConfig -> TVar CacheState -> IO ()
backgroundRefresh :: RefreshConfig -> TVar CacheState -> IO ()
backgroundRefresh RefreshConfig
cfg TVar CacheState
stateVar = do
now <- RefreshConfig -> IO UTCTime
rcClock RefreshConfig
cfg
permitted <- gatedMint cfg stateVar now
when permitted $ do
result <- try (rcMint cfg)
now' <- rcClock cfg
case result of
Right AuthToken
token | UTCTime -> AuthToken -> Bool
tokenValid UTCTime
now' AuthToken
token -> RefreshConfig -> TVar CacheState -> UTCTime -> AuthToken -> IO ()
recordMintSuccess RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now' AuthToken
token
Right AuthToken
_ -> RefreshConfig -> TVar CacheState -> UTCTime -> IO ()
recordMintFailure RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now'
Left (SomeException
_ :: SomeException) -> RefreshConfig -> TVar CacheState -> UTCTime -> IO ()
recordMintFailure RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now'
mintSynchronously :: RefreshConfig -> TVar CacheState -> IO AuthToken
mintSynchronously :: RefreshConfig -> TVar CacheState -> IO AuthToken
mintSynchronously RefreshConfig
cfg TVar CacheState
stateVar = do
now <- RefreshConfig -> IO UTCTime
rcClock RefreshConfig
cfg
permitted <- gatedMint cfg stateVar now
unless permitted (throwIO BreakerOpen)
result <- try (rcMint cfg)
now' <- rcClock cfg
case result of
Right AuthToken
token | UTCTime -> AuthToken -> Bool
tokenValid UTCTime
now' AuthToken
token -> do
RefreshConfig -> TVar CacheState -> UTCTime -> AuthToken -> IO ()
recordMintSuccess RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now' AuthToken
token
AuthToken -> IO AuthToken
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure AuthToken
token
Right AuthToken
_ -> do
RefreshConfig -> TVar CacheState -> UTCTime -> IO ()
recordMintFailure RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now'
CredentialError -> IO AuthToken
forall (m :: * -> *) e a. (MonadIO m, Exception e) => e -> m a
throwIO CredentialError
MintedTokenAlreadyExpired
Left (SomeException
e :: SomeException) -> do
RefreshConfig -> TVar CacheState -> UTCTime -> IO ()
recordMintFailure RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now'
SomeException -> IO AuthToken
forall (m :: * -> *) e a. (MonadIO m, Exception e) => e -> m a
throwIO SomeException
e
releaseSingleFlight :: TVar CacheState -> IO ()
releaseSingleFlight :: TVar CacheState -> IO ()
releaseSingleFlight TVar CacheState
stateVar =
STM () -> IO ()
forall (m :: * -> *) a. MonadIO m => STM a -> m a
atomically (TVar CacheState -> (CacheState -> CacheState) -> STM ()
forall a. TVar a -> (a -> a) -> STM ()
modifyTVar' TVar CacheState
stateVar (\CacheState
st -> CacheState
st{csRefreshing = False}))
admitMint :: TVar CacheState -> UTCTime -> STM Bool
admitMint :: TVar CacheState -> UTCTime -> STM Bool
admitMint TVar CacheState
stateVar UTCTime
now = (\(Bool
permitted, Breaker
_, Breaker
_) -> Bool
permitted) ((Bool, Breaker, Breaker) -> Bool)
-> STM (Bool, Breaker, Breaker) -> STM Bool
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> TVar CacheState -> UTCTime -> STM (Bool, Breaker, Breaker)
admitMintTxn TVar CacheState
stateVar UTCTime
now
admitMintTxn :: TVar CacheState -> UTCTime -> STM (Bool, Breaker, Breaker)
admitMintTxn :: TVar CacheState -> UTCTime -> STM (Bool, Breaker, Breaker)
admitMintTxn TVar CacheState
stateVar UTCTime
now = do
st <- TVar CacheState -> STM CacheState
forall a. TVar a -> STM a
readTVar TVar CacheState
stateVar
let old = CacheState -> Breaker
csBreaker CacheState
st
(permitted, new) = admit now old
writeTVar stateVar st{csBreaker = new}
pure (permitted, old, new)
gatedMint :: RefreshConfig -> TVar CacheState -> UTCTime -> IO Bool
gatedMint :: RefreshConfig -> TVar CacheState -> UTCTime -> IO Bool
gatedMint RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now = do
(permitted, old, new) <- STM (Bool, Breaker, Breaker) -> IO (Bool, Breaker, Breaker)
forall (m :: * -> *) a. MonadIO m => STM a -> m a
atomically (TVar CacheState -> UTCTime -> STM (Bool, Breaker, Breaker)
admitMintTxn TVar CacheState
stateVar UTCTime
now)
reportBreakerChange (rcBreakerReporter cfg) old new
pure permitted
recordMintSuccess :: RefreshConfig -> TVar CacheState -> UTCTime -> AuthToken -> IO ()
recordMintSuccess :: RefreshConfig -> TVar CacheState -> UTCTime -> AuthToken -> IO ()
recordMintSuccess RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now' AuthToken
token = do
due <- RefreshConfig -> UTCTime -> AuthToken -> IO (Maybe UTCTime)
refreshDueAt RefreshConfig
cfg UTCTime
now' AuthToken
token
commitBreakerFold cfg stateVar (onMintSuccess token due)
onRefreshSucceeded (rcRefreshReporter cfg) (ttlSecondsOf now' token)
recordMintFailure :: RefreshConfig -> TVar CacheState -> UTCTime -> IO ()
recordMintFailure :: RefreshConfig -> TVar CacheState -> UTCTime -> IO ()
recordMintFailure RefreshConfig
cfg TVar CacheState
stateVar UTCTime
now' = do
cached <- CacheState -> AuthToken
csToken (CacheState -> AuthToken) -> IO CacheState -> IO AuthToken
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> TVar CacheState -> IO CacheState
forall (m :: * -> *) a. MonadIO m => TVar a -> m a
readTVarIO TVar CacheState
stateVar
commitBreakerFold cfg stateVar (onMintFailure cfg now')
onRefreshFailed (rcRefreshReporter cfg) (ttlSecondsOf now' cached)
commitBreakerFold :: RefreshConfig -> TVar CacheState -> (CacheState -> CacheState) -> IO ()
commitBreakerFold :: RefreshConfig
-> TVar CacheState -> (CacheState -> CacheState) -> IO ()
commitBreakerFold RefreshConfig
cfg TVar CacheState
stateVar CacheState -> CacheState
step = do
(old, new) <- STM (Breaker, Breaker) -> IO (Breaker, Breaker)
forall (m :: * -> *) a. MonadIO m => STM a -> m a
atomically (STM (Breaker, Breaker) -> IO (Breaker, Breaker))
-> STM (Breaker, Breaker) -> IO (Breaker, Breaker)
forall a b. (a -> b) -> a -> b
$ do
st <- TVar CacheState -> STM CacheState
forall a. TVar a -> STM a
readTVar TVar CacheState
stateVar
let st' = CacheState -> CacheState
step CacheState
st
writeTVar stateVar st'
pure (csBreaker st, csBreaker st')
reportBreakerChange (rcBreakerReporter cfg) old new
ttlSecondsOf :: UTCTime -> AuthToken -> Maybe Int
ttlSecondsOf :: UTCTime -> AuthToken -> Maybe Int
ttlSecondsOf UTCTime
now AuthToken
token = case AuthToken -> Maybe UTCTime
authExpiresAt AuthToken
token of
Maybe UTCTime
Nothing -> Maybe Int
forall a. Maybe a
Nothing
Just UTCTime
expiry -> Int -> Maybe Int
forall a. a -> Maybe a
Just (Int -> Int -> Int
forall a. Ord a => a -> a -> a
max Int
0 (NominalDiffTime -> Int
forall b. Integral b => NominalDiffTime -> b
forall a b. (RealFrac a, Integral b) => a -> b
floor (UTCTime -> UTCTime -> NominalDiffTime
diffUTCTime UTCTime
expiry UTCTime
now)))
onMintSuccess :: AuthToken -> Maybe UTCTime -> CacheState -> CacheState
onMintSuccess :: AuthToken -> Maybe UTCTime -> CacheState -> CacheState
onMintSuccess AuthToken
token Maybe UTCTime
due CacheState
st =
CacheState
st
{ csToken = token
, csRefreshDue = due
, csBreaker = recordSuccess (csBreaker st)
}
onMintFailure :: RefreshConfig -> UTCTime -> CacheState -> CacheState
onMintFailure :: RefreshConfig -> UTCTime -> CacheState -> CacheState
onMintFailure RefreshConfig
cfg UTCTime
now CacheState
st =
CacheState
st{csBreaker = recordFailure (rcBreakerThreshold cfg) (rcBreakerCooldown cfg) now (csBreaker st)}
tokenValid :: UTCTime -> AuthToken -> Bool
tokenValid :: UTCTime -> AuthToken -> Bool
tokenValid UTCTime
now AuthToken
token = case AuthToken -> Maybe UTCTime
authExpiresAt AuthToken
token of
Maybe UTCTime
Nothing -> Bool
True
Just UTCTime
expiry -> UTCTime
now UTCTime -> UTCTime -> Bool
forall a. Ord a => a -> a -> Bool
< UTCTime
expiry
refreshNeeded :: UTCTime -> CacheState -> Bool
refreshNeeded :: UTCTime -> CacheState -> Bool
refreshNeeded UTCTime
now CacheState
st = case CacheState -> Maybe UTCTime
csRefreshDue CacheState
st of
Maybe UTCTime
Nothing -> Bool
False
Just UTCTime
due -> UTCTime
now UTCTime -> UTCTime -> Bool
forall a. Ord a => a -> a -> Bool
>= UTCTime
due
refreshDueAt :: RefreshConfig -> UTCTime -> AuthToken -> IO (Maybe UTCTime)
refreshDueAt :: RefreshConfig -> UTCTime -> AuthToken -> IO (Maybe UTCTime)
refreshDueAt RefreshConfig
cfg UTCTime
issuedAt AuthToken
token = case AuthToken -> Maybe UTCTime
authExpiresAt AuthToken
token of
Maybe UTCTime
Nothing -> Maybe UTCTime -> IO (Maybe UTCTime)
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Maybe UTCTime
forall a. Maybe a
Nothing
Just UTCTime
expiry -> do
jitter <- RefreshConfig -> IO Double
rcJitter RefreshConfig
cfg
let lifetime = NominalDiffTime -> Double
forall a b. (Real a, Fractional b) => a -> b
realToFrac (UTCTime -> UTCTime -> NominalDiffTime
diffUTCTime UTCTime
expiry UTCTime
issuedAt) :: Double
frac = Double -> Double
clamp01 (RefreshConfig -> Double
rcRefreshAt RefreshConfig
cfg Double -> Double -> Double
forall a. Num a => a -> a -> a
- Double -> Double
clamp01 Double
jitter)
byFraction = NominalDiffTime -> UTCTime -> UTCTime
addUTCTime (Double -> NominalDiffTime
forall a b. (Real a, Fractional b) => a -> b
realToFrac (Double
frac Double -> Double -> Double
forall a. Num a => a -> a -> a
* Double
lifetime)) UTCTime
issuedAt
floorInstant = NominalDiffTime -> UTCTime -> UTCTime
addUTCTime (NominalDiffTime -> NominalDiffTime
forall a. Num a => a -> a
negate (RefreshConfig -> NominalDiffTime
rcRefreshFloor RefreshConfig
cfg)) UTCTime
expiry
due = UTCTime -> UTCTime -> UTCTime
forall a. Ord a => a -> a -> a
max UTCTime
issuedAt (UTCTime -> UTCTime -> UTCTime
forall a. Ord a => a -> a -> a
min UTCTime
byFraction UTCTime
floorInstant)
pure (Just due)
where
clamp01 :: Double -> Double
clamp01 :: Double -> Double
clamp01 = Double -> Double -> Double
forall a. Ord a => a -> a -> a
max Double
0 (Double -> Double) -> (Double -> Double) -> Double -> Double
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Double -> Double -> Double
forall a. Ord a => a -> a -> a
min Double
1