packages feed

caching-vault-0.1.0.0: src/Data/Cache/Vault.hs

module Data.Cache.Vault
  ( Cache, newCache
  , Key, mintLabeledKey, mintUniqKey
  , insert, delete, reset, lookup
  )
where

import Control.Concurrent.STM (atomically)
import Data.IORef
import Data.Time
import Data.Typeable
import GHC.Exts
import GHC.Fingerprint
import Prelude hiding (lookup)
import System.IO.Unsafe (unsafePerformIO)
import Unsafe.Coerce (unsafeCoerce)
import qualified Data.Text as T
import qualified StmContainers.Map as M

type KeyRepr = T.Text

data CacheEntry
  = CacheEntry
  { ceValidUntil :: Maybe UTCTime
  , ceValue :: Any
  }

newtype Cache
  = Cache { _unCache :: M.Map KeyRepr CacheEntry }

newtype Key a
  = Key { unKey :: KeyRepr }
  deriving (Show, Eq)

keyCounter :: IORef Int
keyCounter =
  unsafePerformIO $ newIORef 0
{-# NOINLINE keyCounter #-}

-- | Mint a globally unique key
mintUniqKey :: IO (Key a)
mintUniqKey =
  atomicModifyIORef' keyCounter $ \ctr ->
  ( ctr + 1
  , Key $ "uniq/" <> T.pack (show ctr)
  )

-- | Mint a key with a label for a given type. Note that keys
-- with the same label but for different types are different.
mintLabeledKey :: forall a. Typeable a => T.Text -> Key a
mintLabeledKey label =
  Key $ "label/" <> label <> "/" <> typeSig
  where
    typeSig =
      let (Fingerprint x1 x2) =
            typeRepFingerprint (typeRep (Proxy :: Proxy (Proxy a)))
      in T.pack $ show x1 <> "." <> show x2

-- | Create a new cache container.
newCache :: IO Cache
newCache =
  Cache <$> M.newIO

-- | Insert a value into the cache with an optional expiry date.
insert :: Key a -> Maybe UTCTime -> a -> Cache -> IO ()
insert k t v (Cache ref) =
  atomically $ M.insert val key ref
  where
    val =
      CacheEntry
      { ceValidUntil = t
      , ceValue = unsafeCoerce v
      }
    key = unKey k

-- | Delete a value from the cache.
delete :: Key a -> Cache -> IO ()
delete k (Cache ref) =
  atomically $ M.delete (unKey k) ref

-- | Purge all values form the cache.
reset :: Cache -> IO ()
reset (Cache ref) =
  atomically $ M.reset ref

-- | Given the current time, lookup a key in the cache.
lookup :: UTCTime -> Key a -> Cache -> IO (Maybe a)
lookup now k (Cache ref) =
  do entry <-
       atomically $ M.lookup (unKey k) ref
     case entry of
       Nothing -> pure Nothing
       Just e ->
         case ceValidUntil e of
           Just validUntil | validUntil < now -> pure Nothing
           _ -> pure (Just $ unsafeCoerce (ceValue e))